update
This commit is contained in:
@@ -12,6 +12,9 @@ from .idrac_routes import register_idrac_routes
|
||||
from .catalog_sync import catalog_bp
|
||||
from .scp_routes import scp_bp
|
||||
from .drm_sync import drm_sync_bp
|
||||
from .server_config_routes import server_config_bp
|
||||
from .bios_baseline_routes import register_bios_baseline_routes
|
||||
from .script_manager import script_manager_bp
|
||||
|
||||
|
||||
def register_routes(app: Flask, socketio=None) -> None:
|
||||
@@ -28,3 +31,14 @@ def register_routes(app: Flask, socketio=None) -> None:
|
||||
app.register_blueprint(catalog_bp)
|
||||
app.register_blueprint(scp_bp)
|
||||
app.register_blueprint(drm_sync_bp)
|
||||
app.register_blueprint(server_config_bp)
|
||||
register_bios_baseline_routes(app)
|
||||
app.register_blueprint(script_manager_bp)
|
||||
|
||||
# CSRF 제외 - server_config API 엔드포인트
|
||||
try:
|
||||
csrf = app.extensions.get('csrf')
|
||||
if csrf:
|
||||
csrf.exempt(server_config_bp)
|
||||
except Exception as e:
|
||||
app.logger.warning(f"CSRF exemption failed for server_config_bp: {e}")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,789 @@
|
||||
"""
|
||||
BIOS Baseline 관리 라우트
|
||||
backend/routes/bios_baseline_routes.py
|
||||
|
||||
BIOS Baseline 생성, 조회, 수정, 삭제 및 비교 기능 제공
|
||||
"""
|
||||
|
||||
from flask import Blueprint, render_template, request, jsonify, current_app
|
||||
from backend.models.user import db
|
||||
from backend.models.bios_baseline import BiosBaseline
|
||||
from backend.services.idrac_redfish_client import DellRedfishClient
|
||||
from datetime import datetime
|
||||
import json
|
||||
from sqlalchemy import text
|
||||
|
||||
# 비교 제외 대상 키 (서버별 고유값 등)
|
||||
IGNORED_KEYS = {
|
||||
'BIOS': {
|
||||
'ServiceTag', 'AssetTag', 'SerialNumber', 'SystemServiceTag', 'SystemAssetTag', 'SystemSerialNumber'
|
||||
},
|
||||
'iDRAC': {
|
||||
'SystemID', 'DNSDomainName', 'DNSRacName', 'DNSDomainNameFromDHCP',
|
||||
'IPv4.1.Address', 'IPv4.1.Gateway', 'IPv4.1.Netmask',
|
||||
'IPv4Static.1.Address', 'IPv4Static.1.Gateway', 'IPv4Static.1.Netmask',
|
||||
'IPv6.1.Address', 'IPv6.1.Gateway',
|
||||
'MACAddress', 'PermanentMACAddress'
|
||||
}
|
||||
}
|
||||
|
||||
def compare_dictionaries(expected, actual, section_name, diff_list, metadata=None):
|
||||
"""딕셔너리 비교 헬퍼 함수"""
|
||||
if not expected:
|
||||
return 0, 0
|
||||
|
||||
matched = 0
|
||||
mismatched = 0
|
||||
|
||||
ignored = IGNORED_KEYS.get(section_name, set())
|
||||
|
||||
for key, exp_val in expected.items():
|
||||
if key in ignored:
|
||||
continue
|
||||
|
||||
act_val = actual.get(key)
|
||||
# 문자열로 변환하여 비교 (타입 불일치 방지)
|
||||
if str(act_val) == str(exp_val):
|
||||
matched += 1
|
||||
# 일치하는 항목도 결과에 포함
|
||||
diff_item = {
|
||||
'section': section_name,
|
||||
'setting_name': key,
|
||||
'expected': exp_val,
|
||||
'actual': act_val,
|
||||
'status': 'match'
|
||||
}
|
||||
if metadata and key in metadata:
|
||||
diff_item['display_name'] = metadata[key]
|
||||
diff_list.append(diff_item)
|
||||
else:
|
||||
mismatched += 1
|
||||
|
||||
diff_item = {
|
||||
'section': section_name,
|
||||
'setting_name': key,
|
||||
'expected': exp_val,
|
||||
'actual': act_val,
|
||||
'status': 'mismatch'
|
||||
}
|
||||
|
||||
# Display Name 추가
|
||||
if metadata and key in metadata:
|
||||
diff_item['display_name'] = metadata[key]
|
||||
|
||||
diff_list.append(diff_item)
|
||||
return matched, mismatched
|
||||
|
||||
def compare_raid_config(expected, actual, diff_list):
|
||||
"""RAID 구성 비교"""
|
||||
if not expected or 'Controllers' not in expected:
|
||||
return 0, 0
|
||||
|
||||
matched = 0
|
||||
mismatched = 0
|
||||
total_checks = 0
|
||||
|
||||
# 컨트롤러별 비교
|
||||
exp_ctrls = {c.get('Name'): c for c in expected.get('Controllers', [])}
|
||||
act_ctrls = {c.get('Name'): c for c in actual.get('Controllers', [])}
|
||||
|
||||
for name, exp_c in exp_ctrls.items():
|
||||
act_c = act_ctrls.get(name)
|
||||
|
||||
if not act_c:
|
||||
mismatched += 1
|
||||
diff_list.append({
|
||||
'section': 'RAID',
|
||||
'setting_name': f"Controller [{name}]",
|
||||
'expected': "Present",
|
||||
'actual': "Missing",
|
||||
'status': 'mismatch'
|
||||
})
|
||||
continue
|
||||
|
||||
matched += 1 # 컨트롤러 존재함
|
||||
diff_list.append({
|
||||
'section': 'RAID',
|
||||
'setting_name': f"Controller [{name}]",
|
||||
'expected': "Present",
|
||||
'actual': "Present",
|
||||
'status': 'match'
|
||||
})
|
||||
|
||||
# 펌웨어 버전 비교
|
||||
exp_fw = exp_c.get('FirmwareVersion')
|
||||
act_fw = act_c.get('FirmwareVersion')
|
||||
if exp_fw and (exp_fw == act_fw):
|
||||
matched += 1
|
||||
diff_list.append({
|
||||
'section': 'RAID',
|
||||
'setting_name': f"Controller [{name}] Firmware",
|
||||
'expected': exp_fw,
|
||||
'actual': act_fw,
|
||||
'status': 'match'
|
||||
})
|
||||
else:
|
||||
mismatched += 1
|
||||
diff_list.append({
|
||||
'section': 'RAID',
|
||||
'setting_name': f"Controller [{name}] Firmware",
|
||||
'expected': exp_fw,
|
||||
'actual': act_fw,
|
||||
'status': 'mismatch'
|
||||
})
|
||||
|
||||
# Volumes (Virtual Disks) 비교
|
||||
exp_vols = {v.get('Name'): v for v in exp_c.get('Volumes', [])}
|
||||
act_vols = {v.get('Name'): v for v in act_c.get('Volumes', [])}
|
||||
|
||||
# Volume 개수 확인
|
||||
if len(exp_vols) != len(act_vols):
|
||||
diff_list.append({
|
||||
'section': 'RAID',
|
||||
'setting_name': f"Controller [{name}] Volume Count",
|
||||
'expected': len(exp_vols),
|
||||
'actual': len(act_vols),
|
||||
'status': 'warning'
|
||||
})
|
||||
|
||||
for vname, exp_v in exp_vols.items():
|
||||
act_v = act_vols.get(vname)
|
||||
if not act_v:
|
||||
mismatched += 1
|
||||
diff_list.append({
|
||||
'section': 'RAID',
|
||||
'setting_name': f"Volume [{name}/{vname}]",
|
||||
'expected': "Present",
|
||||
'actual': "Missing",
|
||||
'status': 'mismatch'
|
||||
})
|
||||
continue
|
||||
|
||||
# RAID Level 비교
|
||||
if exp_v.get('RaidType') == act_v.get('RaidType'):
|
||||
matched += 1
|
||||
diff_list.append({
|
||||
'section': 'RAID',
|
||||
'setting_name': f"Volume [{name}/{vname}] RAID Level",
|
||||
'expected': exp_v.get('RaidType'),
|
||||
'actual': act_v.get('RaidType'),
|
||||
'status': 'match'
|
||||
})
|
||||
else:
|
||||
mismatched += 1
|
||||
diff_list.append({
|
||||
'section': 'RAID',
|
||||
'setting_name': f"Volume [{name}/{vname}] RAID Level",
|
||||
'expected': exp_v.get('RaidType'),
|
||||
'actual': act_v.get('RaidType'),
|
||||
'status': 'mismatch'
|
||||
})
|
||||
|
||||
# 용량 비교 (오차 허용 없이)
|
||||
if exp_v.get('CapacityBytes') == act_v.get('CapacityBytes'):
|
||||
matched += 1
|
||||
diff_list.append({
|
||||
'section': 'RAID',
|
||||
'setting_name': f"Volume [{name}/{vname}] Capacity",
|
||||
'expected': f"{exp_v.get('CapacityBytes', 0) / (1024**3):.2f} GB",
|
||||
'actual': f"{act_v.get('CapacityBytes', 0) / (1024**3):.2f} GB",
|
||||
'status': 'match'
|
||||
})
|
||||
else:
|
||||
mismatched += 1
|
||||
diff_list.append({
|
||||
'section': 'RAID',
|
||||
'setting_name': f"Volume [{name}/{vname}] Capacity",
|
||||
'expected': f"{exp_v.get('CapacityBytes', 0) / (1024**3):.2f} GB",
|
||||
'actual': f"{act_v.get('CapacityBytes', 0) / (1024**3):.2f} GB",
|
||||
'status': 'mismatch'
|
||||
})
|
||||
|
||||
return matched, mismatched
|
||||
|
||||
bios_baseline_bp = Blueprint('bios_baseline', __name__, url_prefix='/bios-baseline')
|
||||
|
||||
# ========================================
|
||||
# 페이지 라우트
|
||||
# ========================================
|
||||
|
||||
@bios_baseline_bp.route('/')
|
||||
def index():
|
||||
"""BIOS Baseline 비교 메인 페이지"""
|
||||
return render_template('bios_baseline.html')
|
||||
|
||||
# ========================================
|
||||
# Baseline 관리 API
|
||||
# ========================================
|
||||
|
||||
@bios_baseline_bp.route('/api/baselines', methods=['GET'])
|
||||
def get_baselines():
|
||||
"""
|
||||
등록된 baseline 목록 조회
|
||||
Query params:
|
||||
- server_model: 특정 모델 필터링
|
||||
- server_type: 특정 타입 필터링
|
||||
- include_data: true면 bios_data도 포함
|
||||
"""
|
||||
try:
|
||||
server_model = request.args.get('server_model')
|
||||
server_type = request.args.get('server_type')
|
||||
include_data = request.args.get('include_data', 'false').lower() == 'true'
|
||||
|
||||
query = BiosBaseline.query.filter_by(is_active=True)
|
||||
|
||||
if server_model:
|
||||
query = query.filter_by(server_model=server_model)
|
||||
if server_type:
|
||||
query = query.filter_by(server_type=server_type)
|
||||
|
||||
baselines = query.order_by(BiosBaseline.created_at.desc()).all()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'baselines': [b.to_dict(include_data=include_data) for b in baselines]
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': str(e)})
|
||||
|
||||
@bios_baseline_bp.route('/api/baselines/<int:baseline_id>', methods=['GET'])
|
||||
def get_baseline(baseline_id):
|
||||
"""특정 baseline 상세 조회 (bios_data 포함)"""
|
||||
try:
|
||||
baseline = BiosBaseline.query.get(baseline_id)
|
||||
if not baseline:
|
||||
return jsonify({'success': False, 'message': 'Baseline을 찾을 수 없습니다'})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'baseline': baseline.to_dict(include_data=True)
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': str(e)})
|
||||
|
||||
def compare_nic_config(baseline_nic, target_nic, diff_list):
|
||||
"""NIC 구성 비교 (ID 기준 매칭)"""
|
||||
matched = 0
|
||||
mismatched = 0
|
||||
|
||||
if not baseline_nic and not target_nic:
|
||||
return 0, 0
|
||||
if not baseline_nic:
|
||||
diff_list.append({'section': 'NIC', 'setting_name': 'NIC Configuration', 'expected': 'Present', 'actual': 'Missing', 'status': 'mismatch'})
|
||||
return 0, 1
|
||||
if not target_nic:
|
||||
diff_list.append({'section': 'NIC', 'setting_name': 'NIC Configuration', 'expected': 'Missing', 'actual': 'Present', 'status': 'mismatch'})
|
||||
return 0, 1
|
||||
|
||||
# Adapters 리스트를 ID 기준 딕셔너리로 변환
|
||||
def to_dict_by_id(adapters):
|
||||
return {a.get('Id', 'Unknown'): a for a in adapters.get('Adapters', [])}
|
||||
|
||||
base_map = to_dict_by_id(baseline_nic)
|
||||
target_map = to_dict_by_id(target_nic)
|
||||
|
||||
all_ids = set(base_map.keys()) | set(target_map.keys())
|
||||
|
||||
for aid in all_ids:
|
||||
b_adp = base_map.get(aid)
|
||||
t_adp = target_map.get(aid)
|
||||
|
||||
if not b_adp:
|
||||
mismatched += 1
|
||||
diff_list.append({'section': 'NIC', 'setting_name': f"NIC [{aid}]", 'expected': 'None', 'actual': 'Added', 'status': 'mismatch'})
|
||||
continue
|
||||
if not t_adp:
|
||||
mismatched += 1
|
||||
diff_list.append({'section': 'NIC', 'setting_name': f"NIC [{aid}]", 'expected': 'Present', 'actual': 'Missing', 'status': 'mismatch'})
|
||||
continue
|
||||
|
||||
matched += 1 # Adapter exists in both
|
||||
diff_list.append({'section': 'NIC', 'setting_name': f"NIC [{aid}]", 'expected': 'Present', 'actual': 'Present', 'status': 'match'})
|
||||
|
||||
# 어댑터 속성 비교 (Firmware 등)
|
||||
for field in ['FirmwareVersion', 'Model']:
|
||||
bv = b_adp.get(field)
|
||||
tv = t_adp.get(field)
|
||||
if bv == tv:
|
||||
matched += 1
|
||||
diff_list.append({'section': 'NIC', 'setting_name': f"NIC [{aid}] {field}", 'expected': bv, 'actual': tv, 'status': 'match'})
|
||||
else:
|
||||
mismatched += 1
|
||||
diff_list.append({'section': 'NIC', 'setting_name': f"NIC [{aid}] {field}", 'expected': bv, 'actual': tv, 'status': 'mismatch'})
|
||||
|
||||
# DeviceFunctions 비교
|
||||
b_funcs = {f.get('Id'): f for f in b_adp.get('DeviceFunctions', [])}
|
||||
t_funcs = {f.get('Id'): f for f in t_adp.get('DeviceFunctions', [])}
|
||||
|
||||
for fid in set(b_funcs.keys()) | set(t_funcs.keys()):
|
||||
bf = b_funcs.get(fid)
|
||||
tf = t_funcs.get(fid)
|
||||
|
||||
prefix = f"NIC [{aid}] Func [{fid}]"
|
||||
|
||||
if not bf:
|
||||
# Baseline에 없는데 Target에 있음 (일반적이지 않음, 설정 변경일 수 있음)
|
||||
# This is an 'added' function, count as mismatch
|
||||
mismatched += 1
|
||||
diff_list.append({'section': 'NIC', 'setting_name': prefix, 'expected': 'None', 'actual': 'Added', 'status': 'mismatch'})
|
||||
continue
|
||||
if not tf:
|
||||
mismatched += 1
|
||||
diff_list.append({'section': 'NIC', 'setting_name': prefix, 'expected': 'Present', 'actual': 'Missing', 'status': 'mismatch'})
|
||||
continue
|
||||
|
||||
matched += 1 # Function exists in both
|
||||
diff_list.append({'section': 'NIC', 'setting_name': prefix, 'expected': 'Present', 'actual': 'Present', 'status': 'match'})
|
||||
|
||||
# 속성 비교 (BootMode, iSCSI, Ethernet)
|
||||
if bf.get('NetBootPMMode') == tf.get('NetBootPMMode'):
|
||||
matched += 1
|
||||
diff_list.append({'section': 'NIC', 'setting_name': f"{prefix} BootMode", 'expected': bf.get('NetBootPMMode'), 'actual': tf.get('NetBootPMMode'), 'status': 'match'})
|
||||
else:
|
||||
mismatched += 1
|
||||
diff_list.append({'section': 'NIC', 'setting_name': f"{prefix} BootMode", 'expected': bf.get('NetBootPMMode'), 'actual': tf.get('NetBootPMMode'), 'status': 'mismatch'})
|
||||
|
||||
# Add more specific NIC settings comparison here if needed
|
||||
# For example, comparing specific Ethernet or iSCSI settings
|
||||
# For now, we'll consider the presence and BootMode.
|
||||
|
||||
return matched, mismatched
|
||||
|
||||
@bios_baseline_bp.route('/api/baselines/create-from-server', methods=['POST'])
|
||||
def create_baseline_from_server():
|
||||
"""
|
||||
Redfish로 서버에서 BIOS 설정을 가져와 새 baseline 생성
|
||||
Request: {
|
||||
"ip_address": "10.10.0.1",
|
||||
"username": "root",
|
||||
"password": "calvin",
|
||||
"name": "R6615_GPU_Standard_2024",
|
||||
"server_type": "GPU Server",
|
||||
"description": "표준 GPU 서버 설정",
|
||||
"created_by": "admin"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = request.json
|
||||
|
||||
# 필수 필드 체크
|
||||
if not all(k in data for k in ['ip_address', 'username', 'password', 'name']):
|
||||
return jsonify({'success': False, 'message': '필수 필드 누락'})
|
||||
|
||||
# 이름 중복 체크
|
||||
if BiosBaseline.query.filter_by(name=data['name'], is_active=True).first():
|
||||
return jsonify({'success': False, 'message': '이미 존재하는 Baseline 이름입니다'})
|
||||
|
||||
# 1. Redfish로 BIOS 설정 가져오기
|
||||
client = DellRedfishClient(data['ip_address'], data['username'], data['password'])
|
||||
|
||||
if not client.check_connection():
|
||||
return jsonify({'success': False, 'message': '서버 연결 실패'})
|
||||
|
||||
# 2. 서버 정보 및 BIOS 설정 가져오기
|
||||
system_info = client.get_system_info_detailed()
|
||||
bios_data = client.get_bios_attributes_all()
|
||||
# BIOS Registry (Display Name) 가져오기
|
||||
bios_metadata = client.get_bios_attribute_registry()
|
||||
|
||||
idrac_data = client.get_idrac_attributes_all()
|
||||
# iDRAC Registry (Display Name) 가져오기
|
||||
idrac_metadata = client.get_idrac_attribute_registry()
|
||||
|
||||
raid_data = client.get_storage_configuration()
|
||||
nic_data = client.get_nic_configuration()
|
||||
boot_data = client.get_boot_settings()
|
||||
firmware_data = client.get_firmware_baseline()
|
||||
|
||||
if not bios_data:
|
||||
return jsonify({'success': False, 'message': 'BIOS 설정 가져오기 실패'})
|
||||
|
||||
# 3. Baseline 생성
|
||||
baseline = BiosBaseline(
|
||||
name=data['name'],
|
||||
server_model=system_info.get('Model', 'Unknown'),
|
||||
server_type=data.get('server_type'),
|
||||
description=data.get('description'),
|
||||
bios_data=bios_data,
|
||||
bios_metadata=bios_metadata, # 메타데이터 저장
|
||||
idrac_data=idrac_data,
|
||||
idrac_metadata=idrac_metadata, # iDRAC 메타데이터 저장
|
||||
raid_data=raid_data,
|
||||
nic_data=nic_data,
|
||||
boot_data=boot_data,
|
||||
firmware_data=firmware_data,
|
||||
source_ip=data['ip_address'],
|
||||
source_service_tag=system_info.get('ServiceTag'),
|
||||
bios_version=bios_data.get('BiosVersion'),
|
||||
total_settings=len(bios_data) + len(idrac_data) + len(raid_data.get('Controllers', [])) + len(nic_data.get('Adapters', [])) + len(boot_data.get('BootSources', [])) + len(firmware_data.get('FirmwareInventory', [])),
|
||||
created_by=data.get('created_by', 'admin')
|
||||
)
|
||||
|
||||
db.session.add(baseline)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Baseline 생성 완료',
|
||||
'baseline': baseline.to_dict()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'message': f'오류: {str(e)}'})
|
||||
|
||||
@bios_baseline_bp.route('/api/baselines/<int:baseline_id>/duplicate', methods=['POST'])
|
||||
def duplicate_baseline(baseline_id):
|
||||
"""
|
||||
기존 baseline을 복사하여 새 baseline 생성
|
||||
Request: {
|
||||
"new_name": "R6615_GPU_ClientA",
|
||||
"description": "고객사 A 맞춤 설정"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = request.json
|
||||
original = BiosBaseline.query.get(baseline_id)
|
||||
|
||||
if not original:
|
||||
return jsonify({'success': False, 'message': '원본 Baseline을 찾을 수 없습니다'})
|
||||
|
||||
# 이름 중복 체크
|
||||
if BiosBaseline.query.filter_by(name=data['new_name'], is_active=True).first():
|
||||
return jsonify({'success': False, 'message': '이미 존재하는 Baseline 이름입니다'})
|
||||
|
||||
# 복사 생성
|
||||
new_baseline = BiosBaseline(
|
||||
name=data['new_name'],
|
||||
server_model=original.server_model,
|
||||
server_type=original.server_type,
|
||||
description=data.get('description', f"{original.name}의 복사본"),
|
||||
bios_data=original.bios_data.copy() if original.bios_data else {},
|
||||
bios_metadata=original.bios_metadata.copy() if original.bios_metadata else {}, # 메타데이터 복사
|
||||
idrac_data=original.idrac_data.copy() if original.idrac_data else {},
|
||||
idrac_metadata=original.idrac_metadata.copy() if original.idrac_metadata else {}, # iDRAC 메타데이터 복사
|
||||
raid_data=original.raid_data.copy() if original.raid_data else {}, # JSON 복사
|
||||
nic_data=original.nic_data.copy() if original.nic_data else {},
|
||||
boot_data=original.boot_data.copy() if original.boot_data else {},
|
||||
firmware_data=original.firmware_data.copy() if original.firmware_data else {},
|
||||
source_ip=original.source_ip,
|
||||
source_service_tag=original.source_service_tag,
|
||||
bios_version=original.bios_version,
|
||||
total_settings=original.total_settings,
|
||||
copied_from_id=original.id,
|
||||
created_by=data.get('created_by', 'admin')
|
||||
)
|
||||
|
||||
db.session.add(new_baseline)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Baseline 복사 완료',
|
||||
'baseline': new_baseline.to_dict()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'message': f'오류: {str(e)}'})
|
||||
|
||||
@bios_baseline_bp.route('/api/baselines/<int:baseline_id>', methods=['PUT'])
|
||||
def update_baseline(baseline_id):
|
||||
"""
|
||||
Baseline 수정 (메타데이터 및 bios_data)
|
||||
Request: {
|
||||
"name": "새 이름",
|
||||
"description": "새 설명",
|
||||
"bios_data": { ... } // 선택사항
|
||||
}
|
||||
"""
|
||||
try:
|
||||
baseline = BiosBaseline.query.get_or_404(baseline_id)
|
||||
data = request.json
|
||||
|
||||
# 이름 변경 시 중복 체크
|
||||
if 'name' in data and data['name'] != baseline.name:
|
||||
if BiosBaseline.query.filter_by(name=data['name'], is_active=True).first():
|
||||
return jsonify({'success': False, 'message': '이미 존재하는 Baseline 이름입니다'})
|
||||
baseline.name = data['name']
|
||||
|
||||
# 메타데이터 업데이트
|
||||
if 'description' in data:
|
||||
baseline.description = data['description']
|
||||
if 'server_type' in data:
|
||||
baseline.server_type = data['server_type']
|
||||
|
||||
# BIOS 데이터 업데이트 (선택사항)
|
||||
if 'bios_data' in data:
|
||||
baseline.bios_data = data['bios_data']
|
||||
if 'bios_metadata' in data:
|
||||
baseline.bios_metadata = data['bios_metadata']
|
||||
if 'idrac_data' in data:
|
||||
baseline.idrac_data = data['idrac_data']
|
||||
if 'idrac_metadata' in data:
|
||||
baseline.idrac_metadata = data['idrac_metadata']
|
||||
if 'raid_data' in data:
|
||||
baseline.raid_data = data['raid_data']
|
||||
if 'nic_data' in data:
|
||||
baseline.nic_data = data['nic_data']
|
||||
if 'boot_data' in data:
|
||||
baseline.boot_data = data['boot_data']
|
||||
if 'firmware_data' in data:
|
||||
baseline.firmware_data = data['firmware_data']
|
||||
|
||||
# 총 설정 수 재계산
|
||||
total = 0
|
||||
if baseline.bios_data: total += len(baseline.bios_data)
|
||||
if baseline.idrac_data: total += len(baseline.idrac_data)
|
||||
if baseline.raid_data and 'Controllers' in baseline.raid_data: total += len(baseline.raid_data['Controllers'])
|
||||
if baseline.nic_data and 'Adapters' in baseline.nic_data: total += len(baseline.nic_data['Adapters'])
|
||||
if baseline.boot_data and 'BootSources' in baseline.boot_data: total += len(baseline.boot_data['BootSources'])
|
||||
if baseline.firmware_data and 'FirmwareInventory' in baseline.firmware_data: total += len(baseline.firmware_data['FirmwareInventory'])
|
||||
baseline.total_settings = total
|
||||
|
||||
baseline.updated_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Baseline 수정 완료',
|
||||
'baseline': baseline.to_dict()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'message': f'오류: {str(e)}'})
|
||||
|
||||
@bios_baseline_bp.route('/api/baselines/<int:baseline_id>', methods=['DELETE'])
|
||||
def delete_baseline(baseline_id):
|
||||
"""Baseline 삭제 (Soft Delete)"""
|
||||
try:
|
||||
baseline = BiosBaseline.query.get(baseline_id)
|
||||
if not baseline:
|
||||
return jsonify({'success': False, 'message': 'Baseline을 찾을 수 없습니다'})
|
||||
|
||||
baseline.is_active = False
|
||||
# 이름 충돌 방지를 위해 이름 변경 (Soft Delete 시)
|
||||
# 예: "MyServer" -> "MyServer_deleted_1678901234"
|
||||
baseline.name = f"{baseline.name}_deleted_{int(datetime.utcnow().timestamp())}"
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'message': 'Baseline 삭제 완료'})
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'message': str(e)})
|
||||
|
||||
# ========================================
|
||||
# 비교 API
|
||||
# ========================================
|
||||
|
||||
@bios_baseline_bp.route('/api/compare', methods=['POST'])
|
||||
def compare_with_baseline():
|
||||
"""
|
||||
IP 주소로 서버 BIOS 설정을 가져와 baseline과 비교
|
||||
Request: {
|
||||
"ip_addresses": ["10.10.0.1", "10.10.0.2"],
|
||||
"baseline_id": 1,
|
||||
"username": "root",
|
||||
"password": "calvin"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = request.json
|
||||
baseline = BiosBaseline.query.get(data['baseline_id'])
|
||||
|
||||
if not baseline:
|
||||
return jsonify({'success': False, 'message': 'Baseline을 찾을 수 없습니다'})
|
||||
|
||||
results = []
|
||||
|
||||
# 병렬 처리를 위한 함수 정의
|
||||
def process_server(ip):
|
||||
try:
|
||||
# 1. Redfish로 현재 BIOS 설정 가져오기
|
||||
client = DellRedfishClient(ip, data['username'], data['password'])
|
||||
|
||||
if not client.check_connection():
|
||||
return {
|
||||
'ip': ip,
|
||||
'status': 'connection_failed',
|
||||
'message': '서버 연결 실패'
|
||||
}
|
||||
|
||||
# 2. Redfish로 현재 설정 가져오기
|
||||
system_info = client.get_system_info_detailed()
|
||||
current_bios = client.get_bios_attributes_all()
|
||||
current_idrac = client.get_idrac_attributes_all()
|
||||
current_raid = client.get_storage_configuration()
|
||||
current_nic = client.get_nic_configuration()
|
||||
current_boot = client.get_boot_settings()
|
||||
current_firmware = client.get_firmware_baseline()
|
||||
|
||||
if not current_bios:
|
||||
return {
|
||||
'ip': ip,
|
||||
'status': 'error',
|
||||
'message': 'BIOS 설정 가져오기 실패'
|
||||
}
|
||||
|
||||
# 3. Baseline과 비교
|
||||
differences = []
|
||||
total_matched = 0
|
||||
total_mismatched = 0
|
||||
|
||||
# 3-1. BIOS 비교
|
||||
b_matched, b_mismatched = compare_dictionaries(baseline.bios_data, current_bios, 'BIOS', differences, baseline.bios_metadata)
|
||||
total_matched += b_matched
|
||||
total_mismatched += b_mismatched
|
||||
|
||||
# 3-2. iDRAC 비교
|
||||
if baseline.idrac_data:
|
||||
i_matched, i_mismatched = compare_dictionaries(baseline.idrac_data, current_idrac, 'iDRAC', differences, baseline.idrac_metadata)
|
||||
total_matched += i_matched
|
||||
total_mismatched += i_mismatched
|
||||
|
||||
# 3-3. RAID 비교
|
||||
if baseline.raid_data:
|
||||
r_matched, r_mismatched = compare_raid_config(baseline.raid_data, current_raid, differences)
|
||||
total_matched += r_matched
|
||||
total_mismatched += r_mismatched
|
||||
|
||||
# 3-4. NIC 비교
|
||||
if baseline.nic_data:
|
||||
n_matched, n_mismatched = compare_nic_config(baseline.nic_data, current_nic, differences)
|
||||
total_matched += n_matched
|
||||
total_mismatched += n_mismatched
|
||||
|
||||
# 3-5. Boot 비교
|
||||
if baseline.boot_data:
|
||||
boot_matched, boot_mismatched = compare_dictionaries(baseline.boot_data, current_boot, 'Boot', differences)
|
||||
total_matched += boot_matched
|
||||
total_mismatched += boot_mismatched
|
||||
|
||||
# 3-6. Firmware 비교
|
||||
if baseline.firmware_data:
|
||||
fw_matched, fw_mismatched = compare_dictionaries(baseline.firmware_data, current_firmware, 'Firmware', differences)
|
||||
total_matched += fw_matched
|
||||
total_mismatched += fw_mismatched
|
||||
|
||||
# 4. 결과 저장
|
||||
match_rate = round((total_matched / (total_matched + total_mismatched)) * 100, 2) if (total_matched + total_mismatched) > 0 else 0
|
||||
|
||||
return {
|
||||
'ip': ip,
|
||||
'status': 'success',
|
||||
'server_model': system_info.get('Model'),
|
||||
'service_tag': system_info.get('ServiceTag'),
|
||||
'bios_version': current_bios.get('BiosVersion'), # Corrected from system_info.get('BiosVersion')
|
||||
'total_items': total_matched + total_mismatched,
|
||||
'matched_items': total_matched,
|
||||
'mismatched_items': total_mismatched,
|
||||
'match_rate': match_rate,
|
||||
'differences': differences
|
||||
}
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc() # Keep traceback for debugging in logs
|
||||
return {
|
||||
'ip': ip,
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}
|
||||
|
||||
import concurrent.futures
|
||||
results = []
|
||||
max_workers = min(len(data['ip_addresses']), 20) # 최대 20개 동시 실행
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
future_to_ip = {executor.submit(process_server, ip): ip for ip in data['ip_addresses']}
|
||||
for future in concurrent.futures.as_completed(future_to_ip):
|
||||
ip = future_to_ip[future]
|
||||
try:
|
||||
result = future.result()
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
results.append({
|
||||
'ip': ip,
|
||||
'status': 'error',
|
||||
'message': f"예상치 못한 오류: {str(e)}"
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'baseline': baseline.to_dict(),
|
||||
'results': results
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': f'오류: {str(e)}'})
|
||||
|
||||
# ========================================
|
||||
# Blueprint 등록 함수
|
||||
# ========================================
|
||||
|
||||
def register_bios_baseline_routes(app):
|
||||
"""
|
||||
bios_baseline_routes 등록 및 초기화
|
||||
"""
|
||||
app.register_blueprint(bios_baseline_bp)
|
||||
|
||||
# CSRF 제외
|
||||
try:
|
||||
csrf = app.extensions.get('csrf')
|
||||
if csrf:
|
||||
csrf.exempt(bios_baseline_bp)
|
||||
except Exception as e:
|
||||
app.logger.warning(f"CSRF exemption failed for bios_baseline_bp: {e}")
|
||||
|
||||
# DB 마이그레이션: 필요한 컬럼이 없으면 추가
|
||||
# SQLite 등에서 ALTER TABLE은 제한적일 수 있으나 여기서는 단순 컬럼 추가 시도
|
||||
with app.app_context():
|
||||
inspector = db.inspect(db.engine)
|
||||
|
||||
# Ensure table exists before checking columns
|
||||
db.create_all()
|
||||
|
||||
columns = [c['name'] for c in inspector.get_columns('bios_baselines')]
|
||||
|
||||
if 'idrac_data' not in columns:
|
||||
print("[INFO] Migrating BIOS Baseline table: Adding idrac_data")
|
||||
with db.engine.connect() as conn:
|
||||
conn.execute(text("ALTER TABLE bios_baselines ADD COLUMN idrac_data JSON"))
|
||||
conn.commit()
|
||||
|
||||
if 'bios_metadata' not in columns:
|
||||
print("[INFO] Migrating BIOS Baseline table: Adding bios_metadata")
|
||||
with db.engine.connect() as conn:
|
||||
conn.execute(text("ALTER TABLE bios_baselines ADD COLUMN bios_metadata JSON"))
|
||||
conn.commit()
|
||||
|
||||
if 'idrac_metadata' not in columns:
|
||||
print("[INFO] Migrating BIOS Baseline table: Adding idrac_metadata")
|
||||
with db.engine.connect() as conn:
|
||||
conn.execute(text("ALTER TABLE bios_baselines ADD COLUMN idrac_metadata JSON"))
|
||||
conn.commit()
|
||||
|
||||
if 'raid_data' not in columns:
|
||||
print("[INFO] Migrating BIOS Baseline table: Adding raid_data")
|
||||
with db.engine.connect() as conn:
|
||||
conn.execute(text("ALTER TABLE bios_baselines ADD COLUMN raid_data JSON"))
|
||||
conn.commit()
|
||||
|
||||
if 'nic_data' not in columns:
|
||||
print("[INFO] Migrating BIOS Baseline table: Adding nic_data")
|
||||
with db.engine.connect() as conn:
|
||||
conn.execute(text("ALTER TABLE bios_baselines ADD COLUMN nic_data JSON"))
|
||||
conn.commit()
|
||||
|
||||
if 'boot_data' not in columns:
|
||||
print("[INFO] Migrating BIOS Baseline table: Adding boot_data")
|
||||
with db.engine.connect() as conn:
|
||||
conn.execute(text("ALTER TABLE bios_baselines ADD COLUMN boot_data JSON"))
|
||||
conn.commit()
|
||||
|
||||
if 'firmware_data' not in columns:
|
||||
print("[INFO] Migrating BIOS Baseline table: Adding firmware_data")
|
||||
with db.engine.connect() as conn:
|
||||
conn.execute(text("ALTER TABLE bios_baselines ADD COLUMN firmware_data JSON"))
|
||||
conn.commit()
|
||||
|
||||
@@ -66,7 +66,21 @@ def view_file():
|
||||
base = Path(Config.BACKUP_FOLDER)
|
||||
safe_date = Path(date).name if date else ""
|
||||
target = (base / safe_date / safe_name).resolve()
|
||||
elif folder == "server_info":
|
||||
base = Path(Config.BACKUP_FOLDER)
|
||||
safe_date = Path(date).name if date else ""
|
||||
target = (base / safe_date / safe_name).resolve()
|
||||
elif folder == "mac":
|
||||
base = Path(Config.MAC_FOLDER)
|
||||
target = (base / safe_name).resolve()
|
||||
elif folder == "guid":
|
||||
base = Path(Config.GUID_FOLDER)
|
||||
target = (base / safe_name).resolve()
|
||||
elif folder == "gpu":
|
||||
base = Path(Config.GPU_FOLDER)
|
||||
target = (base / safe_name).resolve()
|
||||
else:
|
||||
# Default: Processed files (Staging)
|
||||
base = Path(Config.IDRAC_INFO_FOLDER)
|
||||
target = (base / safe_name).resolve()
|
||||
|
||||
|
||||
+104
-971
File diff suppressed because it is too large
Load Diff
@@ -1,669 +0,0 @@
|
||||
"""
|
||||
Dell iDRAC 멀티 서버 펌웨어 관리 라우트
|
||||
backend/routes/idrac_routes.py
|
||||
- CSRF 보호 제외 추가
|
||||
"""
|
||||
|
||||
from flask import Blueprint, render_template, request, jsonify, current_app
|
||||
from werkzeug.utils import secure_filename
|
||||
import os
|
||||
from datetime import datetime
|
||||
from backend.services.idrac_redfish_client import DellRedfishClient
|
||||
from backend.models.idrac_server import IdracServer, db
|
||||
from flask_socketio import emit
|
||||
import threading
|
||||
|
||||
# Blueprint 생성
|
||||
idrac_bp = Blueprint('idrac', __name__, url_prefix='/idrac')
|
||||
|
||||
# 설정
|
||||
UPLOAD_FOLDER = 'uploads/firmware'
|
||||
ALLOWED_EXTENSIONS = {'exe', 'bin'}
|
||||
MAX_FILE_SIZE = 500 * 1024 * 1024 # 500MB
|
||||
|
||||
def allowed_file(filename):
|
||||
"""허용된 파일 형식 확인"""
|
||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
# ========================================
|
||||
# 메인 페이지
|
||||
# ========================================
|
||||
|
||||
@idrac_bp.route('/')
|
||||
def index():
|
||||
"""iDRAC 멀티 서버 관리 메인 페이지"""
|
||||
return render_template('idrac_firmware.html')
|
||||
|
||||
# ========================================
|
||||
# 서버 관리 API
|
||||
# ========================================
|
||||
|
||||
@idrac_bp.route('/api/servers', methods=['GET'])
|
||||
def get_servers():
|
||||
"""등록된 서버 목록 조회"""
|
||||
try:
|
||||
group = request.args.get('group') # 그룹 필터
|
||||
|
||||
query = IdracServer.query.filter_by(is_active=True)
|
||||
if group and group != 'all':
|
||||
query = query.filter_by(group_name=group)
|
||||
|
||||
servers = query.order_by(IdracServer.name).all()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'servers': [s.to_dict() for s in servers]
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'오류: {str(e)}'
|
||||
})
|
||||
|
||||
@idrac_bp.route('/api/servers', methods=['POST'])
|
||||
def add_server():
|
||||
"""서버 추가"""
|
||||
try:
|
||||
data = request.json
|
||||
|
||||
# 필수 필드 확인
|
||||
if not all([data.get('name'), data.get('ip_address'), data.get('password')]):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '필수 필드를 모두 입력하세요 (서버명, IP, 비밀번호)'
|
||||
})
|
||||
|
||||
# 중복 IP 확인
|
||||
existing = IdracServer.query.filter_by(ip_address=data['ip_address']).first()
|
||||
if existing:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'이미 등록된 IP입니다: {data["ip_address"]}'
|
||||
})
|
||||
|
||||
# 서버 생성
|
||||
server = IdracServer(
|
||||
name=data['name'],
|
||||
ip_address=data['ip_address'],
|
||||
username=data.get('username', 'root'),
|
||||
password=data['password'],
|
||||
group_name=data.get('group_name'),
|
||||
location=data.get('location'),
|
||||
model=data.get('model'),
|
||||
notes=data.get('notes')
|
||||
)
|
||||
|
||||
db.session.add(server)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'서버 {server.name} 추가 완료',
|
||||
'server': server.to_dict()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'오류: {str(e)}'
|
||||
})
|
||||
|
||||
@idrac_bp.route('/api/servers/<int:server_id>', methods=['PUT'])
|
||||
def update_server(server_id):
|
||||
"""서버 정보 수정"""
|
||||
try:
|
||||
server = IdracServer.query.get(server_id)
|
||||
if not server:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '서버를 찾을 수 없습니다'
|
||||
})
|
||||
|
||||
data = request.json
|
||||
|
||||
# 업데이트
|
||||
if 'name' in data:
|
||||
server.name = data['name']
|
||||
if 'ip_address' in data:
|
||||
server.ip_address = data['ip_address']
|
||||
if 'username' in data:
|
||||
server.username = data['username']
|
||||
if 'password' in data:
|
||||
server.password = data['password']
|
||||
if 'group_name' in data:
|
||||
server.group_name = data['group_name']
|
||||
if 'location' in data:
|
||||
server.location = data['location']
|
||||
if 'model' in data:
|
||||
server.model = data['model']
|
||||
if 'notes' in data:
|
||||
server.notes = data['notes']
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '서버 정보 수정 완료',
|
||||
'server': server.to_dict()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'오류: {str(e)}'
|
||||
})
|
||||
|
||||
@idrac_bp.route('/api/servers/<int:server_id>', methods=['DELETE'])
|
||||
def delete_server(server_id):
|
||||
"""서버 삭제 (소프트 삭제)"""
|
||||
try:
|
||||
server = IdracServer.query.get(server_id)
|
||||
if not server:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '서버를 찾을 수 없습니다'
|
||||
})
|
||||
|
||||
server.is_active = False
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'서버 {server.name} 삭제 완료'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'오류: {str(e)}'
|
||||
})
|
||||
|
||||
@idrac_bp.route('/api/groups', methods=['GET'])
|
||||
def get_groups():
|
||||
"""등록된 그룹 목록"""
|
||||
try:
|
||||
groups = db.session.query(IdracServer.group_name)\
|
||||
.filter(IdracServer.is_active == True)\
|
||||
.filter(IdracServer.group_name.isnot(None))\
|
||||
.distinct()\
|
||||
.all()
|
||||
|
||||
group_list = [g[0] for g in groups if g[0]]
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'groups': group_list
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'오류: {str(e)}'
|
||||
})
|
||||
|
||||
# ========================================
|
||||
# 연결 및 상태 확인 API
|
||||
# ========================================
|
||||
|
||||
@idrac_bp.route('/api/servers/<int:server_id>/test', methods=['POST'])
|
||||
def test_connection(server_id):
|
||||
"""단일 서버 연결 테스트"""
|
||||
try:
|
||||
server = IdracServer.query.get(server_id)
|
||||
if not server:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '서버를 찾을 수 없습니다'
|
||||
})
|
||||
|
||||
client = DellRedfishClient(server.ip_address, server.username, server.password)
|
||||
|
||||
if client.check_connection():
|
||||
server.status = 'online'
|
||||
server.last_connected = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'{server.name} 연결 성공'
|
||||
})
|
||||
else:
|
||||
server.status = 'offline'
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'{server.name} 연결 실패'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'오류: {str(e)}'
|
||||
})
|
||||
|
||||
@idrac_bp.route('/api/servers/test-multi', methods=['POST'])
|
||||
def test_connections_multi():
|
||||
"""다중 서버 일괄 연결 테스트"""
|
||||
try:
|
||||
data = request.json
|
||||
server_ids = data.get('server_ids', [])
|
||||
|
||||
if not server_ids:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '서버를 선택하세요'
|
||||
})
|
||||
|
||||
results = []
|
||||
|
||||
for server_id in server_ids:
|
||||
server = IdracServer.query.get(server_id)
|
||||
if not server:
|
||||
continue
|
||||
|
||||
try:
|
||||
client = DellRedfishClient(server.ip_address, server.username, server.password)
|
||||
|
||||
if client.check_connection():
|
||||
server.status = 'online'
|
||||
server.last_connected = datetime.utcnow()
|
||||
results.append({
|
||||
'server_id': server.id,
|
||||
'server_name': server.name,
|
||||
'success': True,
|
||||
'message': '연결 성공'
|
||||
})
|
||||
else:
|
||||
server.status = 'offline'
|
||||
results.append({
|
||||
'server_id': server.id,
|
||||
'server_name': server.name,
|
||||
'success': False,
|
||||
'message': '연결 실패'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
server.status = 'offline'
|
||||
results.append({
|
||||
'server_id': server.id,
|
||||
'server_name': server.name,
|
||||
'success': False,
|
||||
'message': str(e)
|
||||
})
|
||||
|
||||
db.session.commit()
|
||||
|
||||
success_count = sum(1 for r in results if r['success'])
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'results': results,
|
||||
'summary': {
|
||||
'total': len(results),
|
||||
'success': success_count,
|
||||
'failed': len(results) - success_count
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'오류: {str(e)}'
|
||||
})
|
||||
|
||||
@idrac_bp.route('/api/servers/<int:server_id>/firmware', methods=['GET'])
|
||||
def get_server_firmware(server_id):
|
||||
"""단일 서버 펌웨어 버전 조회"""
|
||||
try:
|
||||
server = IdracServer.query.get(server_id)
|
||||
if not server:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '서버를 찾을 수 없습니다'
|
||||
})
|
||||
|
||||
client = DellRedfishClient(server.ip_address, server.username, server.password)
|
||||
inventory = client.get_firmware_inventory()
|
||||
|
||||
# BIOS 버전 업데이트
|
||||
for item in inventory:
|
||||
if 'BIOS' in item.get('Name', ''):
|
||||
server.current_bios = item.get('Version')
|
||||
break
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': inventory
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'오류: {str(e)}'
|
||||
})
|
||||
|
||||
# ========================================
|
||||
# 멀티 서버 펌웨어 업로드 API
|
||||
# ========================================
|
||||
|
||||
@idrac_bp.route('/api/upload-multi', methods=['POST'])
|
||||
def upload_multi():
|
||||
"""다중 서버에 펌웨어 일괄 업로드"""
|
||||
try:
|
||||
if 'file' not in request.files:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '파일이 없습니다'
|
||||
})
|
||||
|
||||
file = request.files['file']
|
||||
server_ids_str = request.form.get('server_ids')
|
||||
|
||||
if not server_ids_str:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '서버를 선택하세요'
|
||||
})
|
||||
|
||||
server_ids = [int(x) for x in server_ids_str.split(',')]
|
||||
|
||||
if file.filename == '':
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '파일이 선택되지 않았습니다'
|
||||
})
|
||||
|
||||
if not allowed_file(file.filename):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '허용되지 않은 파일 형식입니다 (.exe, .bin만 가능)'
|
||||
})
|
||||
|
||||
# 로컬에 임시 저장
|
||||
filename = secure_filename(file.filename)
|
||||
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
||||
local_path = os.path.join(UPLOAD_FOLDER, filename)
|
||||
file.save(local_path)
|
||||
|
||||
# 백그라운드 스레드로 업로드 시작
|
||||
from backend.services import watchdog_handler
|
||||
socketio = watchdog_handler.socketio
|
||||
|
||||
def upload_to_servers():
|
||||
results = []
|
||||
|
||||
for idx, server_id in enumerate(server_ids):
|
||||
server = IdracServer.query.get(server_id)
|
||||
if not server:
|
||||
continue
|
||||
|
||||
try:
|
||||
# 진행 상황 전송
|
||||
if socketio:
|
||||
socketio.emit('upload_progress', {
|
||||
'server_id': server.id,
|
||||
'server_name': server.name,
|
||||
'status': 'uploading',
|
||||
'progress': 0,
|
||||
'message': '업로드 시작...'
|
||||
})
|
||||
|
||||
server.status = 'updating'
|
||||
db.session.commit()
|
||||
|
||||
client = DellRedfishClient(server.ip_address, server.username, server.password)
|
||||
result = client.upload_firmware_staged(local_path)
|
||||
|
||||
if result['success']:
|
||||
server.status = 'online'
|
||||
server.last_updated = datetime.utcnow()
|
||||
|
||||
results.append({
|
||||
'server_id': server.id,
|
||||
'server_name': server.name,
|
||||
'success': True,
|
||||
'job_id': result['job_id'],
|
||||
'message': '업로드 완료'
|
||||
})
|
||||
|
||||
if socketio:
|
||||
socketio.emit('upload_progress', {
|
||||
'server_id': server.id,
|
||||
'server_name': server.name,
|
||||
'status': 'completed',
|
||||
'progress': 100,
|
||||
'message': '업로드 완료',
|
||||
'job_id': result['job_id']
|
||||
})
|
||||
else:
|
||||
server.status = 'online'
|
||||
|
||||
results.append({
|
||||
'server_id': server.id,
|
||||
'server_name': server.name,
|
||||
'success': False,
|
||||
'message': result.get('message', '업로드 실패')
|
||||
})
|
||||
|
||||
if socketio:
|
||||
socketio.emit('upload_progress', {
|
||||
'server_id': server.id,
|
||||
'server_name': server.name,
|
||||
'status': 'failed',
|
||||
'progress': 0,
|
||||
'message': result.get('message', '업로드 실패')
|
||||
})
|
||||
|
||||
db.session.commit()
|
||||
|
||||
except Exception as e:
|
||||
server.status = 'online'
|
||||
db.session.commit()
|
||||
|
||||
results.append({
|
||||
'server_id': server.id,
|
||||
'server_name': server.name,
|
||||
'success': False,
|
||||
'message': str(e)
|
||||
})
|
||||
|
||||
if socketio:
|
||||
socketio.emit('upload_progress', {
|
||||
'server_id': server.id,
|
||||
'server_name': server.name,
|
||||
'status': 'failed',
|
||||
'progress': 0,
|
||||
'message': str(e)
|
||||
})
|
||||
|
||||
# 최종 결과 전송
|
||||
if socketio:
|
||||
success_count = sum(1 for r in results if r['success'])
|
||||
socketio.emit('upload_complete', {
|
||||
'results': results,
|
||||
'summary': {
|
||||
'total': len(results),
|
||||
'success': success_count,
|
||||
'failed': len(results) - success_count
|
||||
}
|
||||
})
|
||||
|
||||
# 스레드 시작
|
||||
thread = threading.Thread(target=upload_to_servers)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'{len(server_ids)}대 서버에 업로드 시작',
|
||||
'filename': filename
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'업로드 오류: {str(e)}'
|
||||
})
|
||||
|
||||
# ========================================
|
||||
# 기존 단일 서버 API (호환성 유지)
|
||||
# ========================================
|
||||
|
||||
@idrac_bp.route('/api/files/local', methods=['GET'])
|
||||
def list_local_files():
|
||||
"""로컬에 저장된 DUP 파일 목록"""
|
||||
try:
|
||||
files = []
|
||||
|
||||
if os.path.exists(UPLOAD_FOLDER):
|
||||
for filename in os.listdir(UPLOAD_FOLDER):
|
||||
filepath = os.path.join(UPLOAD_FOLDER, filename)
|
||||
if os.path.isfile(filepath):
|
||||
file_size = os.path.getsize(filepath)
|
||||
files.append({
|
||||
'name': filename,
|
||||
'size': file_size,
|
||||
'size_mb': round(file_size / (1024 * 1024), 2),
|
||||
'uploaded_at': datetime.fromtimestamp(
|
||||
os.path.getmtime(filepath)
|
||||
).strftime('%Y-%m-%d %H:%M:%S')
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'files': files
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'오류: {str(e)}'
|
||||
})
|
||||
|
||||
@idrac_bp.route('/api/files/local/<filename>', methods=['DELETE'])
|
||||
def delete_local_file(filename):
|
||||
"""로컬 DUP 파일 삭제"""
|
||||
try:
|
||||
filepath = os.path.join(UPLOAD_FOLDER, secure_filename(filename))
|
||||
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'{filename} 삭제 완료'
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '파일을 찾을 수 없습니다'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'삭제 오류: {str(e)}'
|
||||
})
|
||||
|
||||
# ========================================
|
||||
# 서버 재부팅 API (멀티)
|
||||
# ========================================
|
||||
|
||||
@idrac_bp.route('/api/servers/reboot-multi', methods=['POST'])
|
||||
def reboot_servers_multi():
|
||||
"""선택한 서버들 일괄 재부팅"""
|
||||
try:
|
||||
data = request.json
|
||||
server_ids = data.get('server_ids', [])
|
||||
reboot_type = data.get('type', 'GracefulRestart')
|
||||
|
||||
if not server_ids:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '서버를 선택하세요'
|
||||
})
|
||||
|
||||
results = []
|
||||
|
||||
for server_id in server_ids:
|
||||
server = IdracServer.query.get(server_id)
|
||||
if not server:
|
||||
continue
|
||||
|
||||
try:
|
||||
client = DellRedfishClient(server.ip_address, server.username, server.password)
|
||||
result = client.reboot_server(reboot_type)
|
||||
|
||||
if result:
|
||||
results.append({
|
||||
'server_id': server.id,
|
||||
'server_name': server.name,
|
||||
'success': True,
|
||||
'message': '재부팅 시작'
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
'server_id': server.id,
|
||||
'server_name': server.name,
|
||||
'success': False,
|
||||
'message': '재부팅 실패'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
results.append({
|
||||
'server_id': server.id,
|
||||
'server_name': server.name,
|
||||
'success': False,
|
||||
'message': str(e)
|
||||
})
|
||||
|
||||
success_count = sum(1 for r in results if r['success'])
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'results': results,
|
||||
'summary': {
|
||||
'total': len(results),
|
||||
'success': success_count,
|
||||
'failed': len(results) - success_count
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'오류: {str(e)}'
|
||||
})
|
||||
|
||||
|
||||
# Blueprint 등록 함수
|
||||
def register_idrac_routes(app):
|
||||
"""
|
||||
iDRAC 멀티 서버 관리 Blueprint 등록
|
||||
|
||||
Args:
|
||||
app: Flask 애플리케이션 인스턴스
|
||||
"""
|
||||
# uploads 디렉토리 생성
|
||||
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
||||
|
||||
# Blueprint 등록
|
||||
app.register_blueprint(idrac_bp)
|
||||
|
||||
# CSRF 보호 제외 - iDRAC API 엔드포인트
|
||||
try:
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
csrf = CSRFProtect()
|
||||
# API 엔드포인트는 CSRF 검증 제외
|
||||
csrf.exempt(idrac_bp)
|
||||
except:
|
||||
pass # CSRF 설정 실패해도 계속 진행
|
||||
|
||||
# DB 테이블 생성
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
+155
-18
@@ -98,7 +98,7 @@ def index():
|
||||
end_page = min(start_page + 9, total_pages)
|
||||
|
||||
# 4. 백업 폴더 목록
|
||||
backup_dirs = [d for d in backup_dir.iterdir() if d.is_dir()]
|
||||
backup_dirs = [d for d in backup_dir.iterdir() if d.is_dir() and d.name != "archive"]
|
||||
backup_dirs.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
|
||||
backup_page = int(request.args.get("backup_page", 1))
|
||||
@@ -119,31 +119,109 @@ def index():
|
||||
total_pages=total_pages,
|
||||
start_page=start_page,
|
||||
end_page=end_page,
|
||||
backup_files=backup_files,
|
||||
backup_files={}, # [DISABLED] Use server_info_files instead
|
||||
total_backup_pages=total_backup_pages,
|
||||
backup_page=backup_page,
|
||||
server_info_files=backup_files, # Server Info (Paginated)
|
||||
scripts=all_scripts, # 기존 리스트 호환
|
||||
grouped_scripts=grouped_scripts_sorted, # 카테고리별 분류
|
||||
xml_files=xml_files,
|
||||
# [NEW] Repositories
|
||||
mac_files=_get_file_info_list(Config.MAC_FOLDER),
|
||||
guid_files=_get_file_info_list(Config.GUID_FOLDER),
|
||||
gpu_files=_get_file_info_list(Config.GPU_FOLDER),
|
||||
)
|
||||
|
||||
|
||||
|
||||
def _get_backup_info(folder_path):
|
||||
"""
|
||||
폴더 내의 하위 폴더들을 순회하며 파일 목록 정보를 반환 (백업/아카이브 용)
|
||||
"""
|
||||
path = Path(folder_path)
|
||||
if not path.exists():
|
||||
return {}
|
||||
|
||||
dirs = [d for d in path.iterdir() if d.is_dir() and d.name != "archive"]
|
||||
dirs.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
|
||||
result = {}
|
||||
for d in dirs:
|
||||
files = [f.name for f in d.iterdir() if f.is_file()]
|
||||
result[d.name] = {"files": files, "count": len(files)}
|
||||
return result
|
||||
|
||||
def _get_file_info_list(folder_path_str):
|
||||
"""
|
||||
폴더 내 파일들의 상세 정보(이름, 크기, 수정일)를 리스트로 반환
|
||||
"""
|
||||
path = Path(folder_path_str)
|
||||
if not path.exists():
|
||||
return []
|
||||
|
||||
files = []
|
||||
try:
|
||||
for f in path.glob("*"):
|
||||
if f.is_file():
|
||||
stat = f.stat()
|
||||
files.append({
|
||||
"name": f.name,
|
||||
"size": stat.st_size,
|
||||
"mtime": stat.st_mtime, # timestamp for sorting
|
||||
"date": time.strftime('%Y-%m-%d %H:%M', time.localtime(stat.st_mtime))
|
||||
})
|
||||
except Exception as e:
|
||||
logging.error(f"Error reading folder {folder_path_str}: {e}")
|
||||
return []
|
||||
|
||||
# 기본 정렬: 최신순
|
||||
return sorted(files, key=lambda x: x['mtime'], reverse=True)
|
||||
|
||||
|
||||
@main_bp.route("/process_ips", methods=["POST"])
|
||||
@login_required
|
||||
def process_ips():
|
||||
ips = request.form.get("ips")
|
||||
selected_script = request.form.get("script")
|
||||
selected_xml_file = request.form.get("xmlFile")
|
||||
|
||||
if not ips or not selected_script:
|
||||
return jsonify({"error": "IP 주소와 스크립트를 모두 입력하세요."}), 400
|
||||
|
||||
job_type = request.form.get("job_type")
|
||||
|
||||
# 변수 초기화
|
||||
selected_script = ""
|
||||
profile_name = None
|
||||
xml_file_path = None
|
||||
if selected_script == "02-set_config.py" and selected_xml_file:
|
||||
xml_path = Path(Config.XML_FOLDER) / selected_xml_file
|
||||
if not xml_path.exists():
|
||||
return jsonify({"error": "선택한 XML 파일이 존재하지 않습니다."}), 400
|
||||
xml_file_path = str(xml_path)
|
||||
slot_priority = request.form.get("slot_priority") # [NEW] 슬롯 우선순위
|
||||
|
||||
# 작업 유형에 따른 스크립트 및 프로파일 설정
|
||||
if job_type in ["mac", "server_info", "guid", "gpu"]:
|
||||
profile_name = request.form.get("profile")
|
||||
if job_type in ["mac", "guid"] and not profile_name:
|
||||
return jsonify({"error": "프로파일을 선택하세요."}), 400
|
||||
|
||||
if job_type == "mac":
|
||||
selected_script = "unified/collect_mac.py"
|
||||
elif job_type == "server_info":
|
||||
selected_script = "unified/collect_server_info.py"
|
||||
elif job_type == "guid":
|
||||
selected_script = "unified/collect_guid.py"
|
||||
elif job_type == "gpu":
|
||||
selected_script = "unified/collect_gpu.py"
|
||||
profile_name = None # GPU는 프로파일 불필요
|
||||
|
||||
else:
|
||||
# Legacy 모드 (기존 스크립트 선택)
|
||||
selected_script = request.form.get("script")
|
||||
selected_xml_file = request.form.get("xmlFile")
|
||||
|
||||
if not selected_script:
|
||||
return jsonify({"error": "스크립트를 선택하세요."}), 400
|
||||
|
||||
if selected_script == "02-set_config.py" and selected_xml_file:
|
||||
xml_path = Path(Config.XML_FOLDER) / selected_xml_file
|
||||
if not xml_path.exists():
|
||||
return jsonify({"error": "선택한 XML 파일이 존재하지 않습니다."}), 400
|
||||
xml_file_path = str(xml_path)
|
||||
|
||||
if not ips:
|
||||
return jsonify({"error": "IP 주소를 입력하세요."}), 400
|
||||
|
||||
job_id = str(time.time())
|
||||
session["job_id"] = job_id
|
||||
@@ -157,11 +235,11 @@ def process_ips():
|
||||
observer.start()
|
||||
|
||||
future = executor.submit(
|
||||
process_ips_concurrently, ip_files, job_id, observer, selected_script, xml_file_path
|
||||
process_ips_concurrently, ip_files, job_id, observer, selected_script, xml_file_path, profile_name, slot_priority
|
||||
)
|
||||
future.add_done_callback(lambda x: on_complete(job_id))
|
||||
|
||||
logging.info(f"[AJAX] 작업 시작: {job_id}, script: {selected_script}")
|
||||
logging.info(f"[AJAX] 작업 시작: {job_id}, type: {job_type}, script: {selected_script}, profile: {profile_name}")
|
||||
return jsonify({"job_id": job_id})
|
||||
|
||||
|
||||
@@ -220,11 +298,21 @@ def delete_file(filename: str):
|
||||
@main_bp.route("/download_zip", methods=["POST"])
|
||||
@login_required
|
||||
def download_zip():
|
||||
zip_filename = request.form.get("zip_filename", "export")
|
||||
zip_filename = request.form.get("zip_filename", "export").strip()
|
||||
zip_path = Path(Config.TEMP_ZIP_FOLDER) / f"{zip_filename}.zip"
|
||||
|
||||
# 기본 대상은 스테이징 폴더 (IDRAC_INFO_FOLDER)
|
||||
target_dir = Path(Config.IDRAC_INFO_FOLDER)
|
||||
|
||||
# 입력된 이름이 백업 폴더에 존재하는지 확인
|
||||
if zip_filename:
|
||||
possible_backup_path = Path(Config.BACKUP_FOLDER) / zip_filename
|
||||
if possible_backup_path.exists() and possible_backup_path.is_dir():
|
||||
target_dir = possible_backup_path
|
||||
logging.info(f"백업 폴더 ZIP 다운로드 요청: {zip_filename}")
|
||||
|
||||
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zipf:
|
||||
for file in Path(Config.IDRAC_INFO_FOLDER).glob("*"):
|
||||
for file in target_dir.glob("*"):
|
||||
if file.is_file():
|
||||
zipf.write(file, arcname=file.name)
|
||||
|
||||
@@ -280,4 +368,53 @@ def move_backup_files():
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"파일 이동 실패: {e}")
|
||||
return jsonify({"success": False, "message": f"이동 중 오류 발생: {str(e)}"}), 500
|
||||
return jsonify({"success": False, "message": f"이동 중 오류 발생: {str(e)}"}), 500
|
||||
|
||||
|
||||
@main_bp.route("/delete_backup_folder/<folder_name>", methods=["POST"])
|
||||
@login_required
|
||||
def delete_backup_folder(folder_name: str):
|
||||
folder_path = Path(Config.BACKUP_FOLDER) / folder_name
|
||||
|
||||
# 기본 폴더 보호
|
||||
if folder_name == "archive":
|
||||
flash("이 폴더는 삭제할 수 없습니다.", "warning")
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
if folder_path.exists() and folder_path.is_dir():
|
||||
try:
|
||||
shutil.rmtree(folder_path)
|
||||
flash(f"백업 폴더 '{folder_name}'가 삭제되었습니다.", "success")
|
||||
logging.info(f"백업 폴더 삭제됨: {folder_name}")
|
||||
except Exception as e:
|
||||
logging.error(f"백업 폴더 삭제 오류: {e}")
|
||||
flash("폴더 삭제 중 오류가 발생했습니다.", "danger")
|
||||
else:
|
||||
flash("폴더가 존재하지 않습니다.", "warning")
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
|
||||
@main_bp.route("/archive_backup_folder/<folder_name>", methods=["POST"])
|
||||
@login_required
|
||||
def archive_backup_folder(folder_name: str):
|
||||
src_path = Path(Config.BACKUP_FOLDER) / folder_name
|
||||
archive_root = Path(Config.ARCHIVE_FOLDER)
|
||||
archive_root.mkdir(parents=True, exist_ok=True)
|
||||
dst_path = archive_root / folder_name
|
||||
|
||||
if src_path.exists() and src_path.is_dir():
|
||||
try:
|
||||
# 이름 중복 시 처리 (timestamp 추가)
|
||||
if dst_path.exists():
|
||||
timestamp = int(time.time())
|
||||
dst_path = archive_root / f"{folder_name}_{timestamp}"
|
||||
|
||||
shutil.move(str(src_path), str(dst_path))
|
||||
flash(f"'{folder_name}' 폴더가 아카이브(archive)로 이동되었습니다.", "success")
|
||||
logging.info(f"백업 아카이빙 성공: {folder_name} -> {dst_path}")
|
||||
except Exception as e:
|
||||
logging.error(f"백업 아카이빙 오류: {e}")
|
||||
flash("폴더 이동 중 오류가 발생했습니다.", "danger")
|
||||
else:
|
||||
flash("폴더가 존재하지 않습니다.", "warning")
|
||||
return redirect(url_for("main.index"))
|
||||
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
스크립트 관리 라우트 및 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)
|
||||
@@ -0,0 +1,185 @@
|
||||
from flask import Blueprint, render_template, request, jsonify
|
||||
from backend.models.user import db
|
||||
from backend.models.idrac_server import IdracServer
|
||||
from backend.models.server_config import ServerConfigSnapshot
|
||||
from backend.services.idrac_redfish_client import DellRedfishClient
|
||||
import concurrent.futures
|
||||
import json
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
|
||||
server_config_bp = Blueprint('server_config', __name__, url_prefix='/server-config')
|
||||
|
||||
@server_config_bp.route('/')
|
||||
def index():
|
||||
"""서버 설정 분석 및 비교 페이지"""
|
||||
return render_template('server_config.html')
|
||||
|
||||
def calculate_checksum(data):
|
||||
"""JSON 데이터의 SHA256 체크섬 계산"""
|
||||
json_str = json.dumps(data, sort_keys=True)
|
||||
return hashlib.sha256(json_str.encode('utf-8')).hexdigest()
|
||||
|
||||
@server_config_bp.route('/api/sync', methods=['POST'])
|
||||
def sync_configs():
|
||||
"""
|
||||
선택한 서버들의 설정을 iDRAC에서 가져와 DB에 저장 (동기화)
|
||||
"""
|
||||
try:
|
||||
data = request.json
|
||||
server_ids = data.get('server_ids', [])
|
||||
categories = data.get('categories', ['bios', 'idrac', 'system'])
|
||||
|
||||
if not server_ids:
|
||||
return jsonify({'success': False, 'message': '서버를 선택해주세요.'})
|
||||
|
||||
results = {'success': [], 'failed': []}
|
||||
|
||||
def process_server(server_id):
|
||||
server = IdracServer.query.get(server_id)
|
||||
if not server:
|
||||
return {'id': server_id, 'status': 'failed', 'message': 'Server not found'}
|
||||
|
||||
client = DellRedfishClient(server.ip_address, server.username, server.password)
|
||||
if not client.check_connection():
|
||||
return {'id': server.id, 'name': server.name, 'status': 'failed', 'message': 'Connection failed'}
|
||||
|
||||
sync_results = []
|
||||
|
||||
# System Info
|
||||
if 'system' in categories:
|
||||
sys_data = client.get_system_info_detailed()
|
||||
if sys_data:
|
||||
save_snapshot(server.id, 'system', sys_data)
|
||||
sync_results.append('system')
|
||||
|
||||
# BIOS Attributes
|
||||
if 'bios' in categories:
|
||||
bios_data = client.get_bios_attributes_all()
|
||||
if bios_data:
|
||||
save_snapshot(server.id, 'bios', bios_data)
|
||||
sync_results.append('bios')
|
||||
|
||||
# iDRAC Attributes
|
||||
if 'idrac' in categories:
|
||||
idrac_data = client.get_idrac_attributes_all()
|
||||
if idrac_data:
|
||||
save_snapshot(server.id, 'idrac', idrac_data)
|
||||
sync_results.append('idrac')
|
||||
|
||||
return {'id': server.id, 'name': server.name, 'status': 'success', 'synced': sync_results}
|
||||
|
||||
def save_snapshot(server_id, config_type, data):
|
||||
# 기존 스냅샷 확인
|
||||
snapshot = ServerConfigSnapshot.query.filter_by(
|
||||
server_id=server_id,
|
||||
config_type=config_type
|
||||
).first()
|
||||
|
||||
new_hash = calculate_checksum(data)
|
||||
|
||||
if snapshot:
|
||||
# 변경사항이 있는 경우에만 업데이트
|
||||
if snapshot.hash_value != new_hash:
|
||||
snapshot.data = data
|
||||
snapshot.hash_value = new_hash
|
||||
snapshot.updated_at = datetime.utcnow() # 명시적 업데이트 시간 갱신
|
||||
else:
|
||||
snapshot = ServerConfigSnapshot(
|
||||
server_id=server_id,
|
||||
config_type=config_type,
|
||||
data=data,
|
||||
hash_value=new_hash
|
||||
)
|
||||
db.session.add(snapshot)
|
||||
|
||||
# 트랜잭션은 개별 commit 혹은 묶어서 commit.
|
||||
# 여기서는 스레드 안전성을 위해 함수 내에서는 session 조작만 하고
|
||||
# 실제 commit은 메인 스레드나 별도 처리 필요하지만, Flask-SQLAlchemy는 scoped_session이므로
|
||||
# 각 요청(스레드)마다 세션이 다를 수 있음.
|
||||
# 하지만 ThreadPoolExecutor 사용 시 app context 문제가 발생할 수 있음.
|
||||
# safe way: return data -> main thread saves. OR push app context.
|
||||
|
||||
# NOTE: For simplicity in this refactor with ThreadPoolExecutor,
|
||||
# we will handle DB operations inside the thread but need app context.
|
||||
# See below for implementation adjustment.
|
||||
return True
|
||||
|
||||
# DB 저장을 위해 app context 필요
|
||||
from flask import current_app
|
||||
app = current_app._get_current_object()
|
||||
|
||||
def process_with_context(server_id):
|
||||
with app.app_context():
|
||||
try:
|
||||
res = process_server(server_id)
|
||||
db.session.commit() # Commit per server
|
||||
return res
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return {'id': server_id, 'status': 'failed', 'message': str(e)}
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
||||
future_to_id = {executor.submit(process_with_context, sid): sid for sid in server_ids}
|
||||
|
||||
for future in concurrent.futures.as_completed(future_to_id):
|
||||
try:
|
||||
res = future.result()
|
||||
if res['status'] == 'success':
|
||||
results['success'].append(res)
|
||||
else:
|
||||
results['failed'].append(res)
|
||||
except Exception as e:
|
||||
results['failed'].append({'error': str(e)})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'results': results
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': f"Sync Error: {str(e)}"})
|
||||
|
||||
@server_config_bp.route('/api/fetch', methods=['POST'])
|
||||
def fetch_configs():
|
||||
"""
|
||||
DB에 저장된 서버 설정 스냅샷 조회
|
||||
"""
|
||||
try:
|
||||
data = request.json
|
||||
server_ids = data.get('server_ids', [])
|
||||
categories = data.get('categories', ['bios', 'idrac', 'system'])
|
||||
|
||||
if not server_ids:
|
||||
return jsonify({'success': False, 'message': '서버를 선택해주세요.'})
|
||||
|
||||
results = {}
|
||||
|
||||
for server_id in server_ids:
|
||||
server = IdracServer.query.get(server_id)
|
||||
if not server:
|
||||
continue
|
||||
|
||||
server_data = {'id': server.id, 'name': server.name, 'ip': server.ip_address}
|
||||
|
||||
for cat in categories:
|
||||
snapshot = ServerConfigSnapshot.query.filter_by(
|
||||
server_id=server_id,
|
||||
config_type=cat
|
||||
).first()
|
||||
|
||||
if snapshot:
|
||||
server_data[cat] = snapshot.data
|
||||
server_data[f'{cat}_updated'] = snapshot.updated_at.isoformat()
|
||||
else:
|
||||
server_data[cat] = None
|
||||
|
||||
results[server_id] = server_data
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'results': results
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': f"Fetch Error: {str(e)}"})
|
||||
@@ -333,7 +333,7 @@ def update_server_list():
|
||||
try:
|
||||
path.write_text(content, encoding="utf-8")
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(Path(Config.SERVER_LIST_FOLDER) / "excel.py")],
|
||||
[sys.executable, str(Path(Config.SERVER_LIST_FOLDER) / "unified_excel_generator.py"), "--mode", "mac"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
@@ -373,7 +373,7 @@ def update_guid_list():
|
||||
logging.info(f"GUID 슬롯 우선순위: {slot_priority}")
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(Path(Config.SERVER_LIST_FOLDER) / "GUIDtxtT0Execl.py")],
|
||||
[sys.executable, str(Path(Config.SERVER_LIST_FOLDER) / "unified_excel_generator.py"), "--mode", "guid"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
@@ -417,8 +417,8 @@ def update_gpu_list():
|
||||
# 2) 엑셀 생성 실행 (GPU 프리셋)
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(script_path),
|
||||
"--preset", "gpu",
|
||||
str(Path(Config.SERVER_LIST_FOLDER) / "unified_excel_generator.py"),
|
||||
"--mode", "gpu",
|
||||
"--list-file", str(list_path),
|
||||
]
|
||||
result = subprocess.run(
|
||||
@@ -522,4 +522,78 @@ def scan_network():
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Scan network fatal error: {e}")
|
||||
return jsonify({"success": False, "error": f"서버 내부 오류: {str(e)}"}), 500
|
||||
return jsonify({"success": False, "error": f"서버 내부 오류: {str(e)}"}), 500
|
||||
|
||||
|
||||
@utils_bp.route("/api/system/control", methods=["POST"])
|
||||
@login_required
|
||||
def system_control():
|
||||
"""
|
||||
시스템 제어 명령 실행 (Power ON/OFF, Log Clear 등)
|
||||
"""
|
||||
data = request.get_json(silent=True) or {}
|
||||
action = data.get("action")
|
||||
ips = data.get("ips", [])
|
||||
|
||||
if not action or not ips:
|
||||
return jsonify({"success": False, "error": "Action 또는 IP 목록이 없습니다."}), 400
|
||||
|
||||
# 통합 스크립트 사용
|
||||
script_name = "unified/system_control.py"
|
||||
script_path = Path(Config.SCRIPT_FOLDER) / script_name
|
||||
|
||||
if not script_path.exists():
|
||||
return jsonify({"success": False, "error": f"스크립트 파일을 찾을 수 없습니다: {script_name}"}), 500
|
||||
|
||||
# 임시 IP 파일 생성
|
||||
import tempfile
|
||||
try:
|
||||
# data/temp/uploads 폴더 사용
|
||||
temp_dir = Path(Config.UPLOAD_FOLDER)
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w+', delete=False, dir=temp_dir, suffix=".txt", encoding='utf-8') as tf:
|
||||
tf.write("\n".join(ips))
|
||||
temp_ip_file = tf.name
|
||||
|
||||
# 스크립트 실행: python unified/system_control.py <action> <ip_file>
|
||||
cmd = [sys.executable, str(script_path), action, temp_ip_file]
|
||||
|
||||
logging.info(f"[SystemControl] Executing {action} with {len(ips)} IPs (Unified Script)...")
|
||||
|
||||
# 타임아웃 넉넉하게 (작업에 따라 다름)
|
||||
timeout = 600 if action in ["tsr_collect", "tsr_save"] else 300
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False, # 에러 발생해도 결과 받음
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
# 임시 파일 삭제
|
||||
try:
|
||||
os.remove(temp_ip_file)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if result.returncode == 0:
|
||||
logging.info(f"[SystemControl] Success: {result.stdout}")
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": f"작업({action})이 성공적으로 완료되었습니다.",
|
||||
"details": result.stdout
|
||||
})
|
||||
else:
|
||||
logging.error(f"[SystemControl] Failed: {result.stderr}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": f"스크립트 실행 중 오류가 발생했습니다.\n{result.stderr}"
|
||||
}), 500
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return jsonify({"success": False, "error": "스크립트 실행 시간이 초과되었습니다."}), 504
|
||||
except Exception as e:
|
||||
logging.error(f"[SystemControl] Exception: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
@@ -1,399 +0,0 @@
|
||||
"""
|
||||
펌웨어 버전 비교 API 코드
|
||||
idrac_routes.py 파일의 register_idrac_routes 함수 위에 추가하세요
|
||||
"""
|
||||
|
||||
# ========================================
|
||||
# 펌웨어 버전 관리 API
|
||||
# ========================================
|
||||
|
||||
@idrac_bp.route('/api/firmware-versions', methods=['GET'])
|
||||
def get_firmware_versions():
|
||||
"""등록된 최신 펌웨어 버전 목록"""
|
||||
try:
|
||||
server_model = request.args.get('model') # 서버 모델 필터
|
||||
|
||||
query = FirmwareVersion.query.filter_by(is_active=True)
|
||||
|
||||
if server_model:
|
||||
# 특정 모델 또는 범용
|
||||
query = query.filter(
|
||||
(FirmwareVersion.server_model == server_model) |
|
||||
(FirmwareVersion.server_model == None)
|
||||
)
|
||||
|
||||
versions = query.order_by(FirmwareVersion.component_name).all()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'versions': [v.to_dict() for v in versions]
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'오류: {str(e)}'
|
||||
})
|
||||
|
||||
@idrac_bp.route('/api/firmware-versions', methods=['POST'])
|
||||
def add_firmware_version():
|
||||
"""최신 펌웨어 버전 등록"""
|
||||
try:
|
||||
data = request.json
|
||||
|
||||
# 필수 필드 확인
|
||||
if not all([data.get('component_name'), data.get('latest_version')]):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '컴포넌트명과 버전을 입력하세요'
|
||||
})
|
||||
|
||||
# 중복 확인 (같은 컴포넌트, 같은 모델)
|
||||
existing = FirmwareVersion.query.filter_by(
|
||||
component_name=data['component_name'],
|
||||
server_model=data.get('server_model')
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'이미 등록된 컴포넌트입니다'
|
||||
})
|
||||
|
||||
# 버전 생성
|
||||
version = FirmwareVersion(
|
||||
component_name=data['component_name'],
|
||||
component_type=data.get('component_type'),
|
||||
vendor=data.get('vendor'),
|
||||
server_model=data.get('server_model'),
|
||||
latest_version=data['latest_version'],
|
||||
release_date=data.get('release_date'),
|
||||
download_url=data.get('download_url'),
|
||||
file_name=data.get('file_name'),
|
||||
file_size_mb=data.get('file_size_mb'),
|
||||
notes=data.get('notes'),
|
||||
is_critical=data.get('is_critical', False)
|
||||
)
|
||||
|
||||
db.session.add(version)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'{version.component_name} 버전 정보 등록 완료',
|
||||
'version': version.to_dict()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'오류: {str(e)}'
|
||||
})
|
||||
|
||||
@idrac_bp.route('/api/firmware-versions/<int:version_id>', methods=['PUT'])
|
||||
def update_firmware_version(version_id):
|
||||
"""펌웨어 버전 정보 수정"""
|
||||
try:
|
||||
version = FirmwareVersion.query.get(version_id)
|
||||
if not version:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '버전 정보를 찾을 수 없습니다'
|
||||
})
|
||||
|
||||
data = request.json
|
||||
|
||||
# 업데이트
|
||||
if 'component_name' in data:
|
||||
version.component_name = data['component_name']
|
||||
if 'latest_version' in data:
|
||||
version.latest_version = data['latest_version']
|
||||
if 'release_date' in data:
|
||||
version.release_date = data['release_date']
|
||||
if 'download_url' in data:
|
||||
version.download_url = data['download_url']
|
||||
if 'file_name' in data:
|
||||
version.file_name = data['file_name']
|
||||
if 'notes' in data:
|
||||
version.notes = data['notes']
|
||||
if 'is_critical' in data:
|
||||
version.is_critical = data['is_critical']
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '버전 정보 수정 완료',
|
||||
'version': version.to_dict()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'오류: {str(e)}'
|
||||
})
|
||||
|
||||
@idrac_bp.route('/api/firmware-versions/<int:version_id>', methods=['DELETE'])
|
||||
def delete_firmware_version(version_id):
|
||||
"""펌웨어 버전 정보 삭제"""
|
||||
try:
|
||||
version = FirmwareVersion.query.get(version_id)
|
||||
if not version:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '버전 정보를 찾을 수 없습니다'
|
||||
})
|
||||
|
||||
version.is_active = False
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'{version.component_name} 버전 정보 삭제 완료'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'오류: {str(e)}'
|
||||
})
|
||||
|
||||
@idrac_bp.route('/api/servers/<int:server_id>/firmware/compare', methods=['GET'])
|
||||
def compare_server_firmware(server_id):
|
||||
"""
|
||||
서버 펌웨어 버전 비교
|
||||
현재 버전과 최신 버전을 비교하여 업데이트 필요 여부 확인
|
||||
"""
|
||||
try:
|
||||
server = IdracServer.query.get(server_id)
|
||||
if not server:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '서버를 찾을 수 없습니다'
|
||||
})
|
||||
|
||||
# 현재 펌웨어 조회
|
||||
client = DellRedfishClient(server.ip_address, server.username, server.password)
|
||||
current_inventory = client.get_firmware_inventory()
|
||||
|
||||
# 최신 버전 정보 조회
|
||||
latest_versions = FirmwareVersion.query.filter_by(is_active=True).all()
|
||||
|
||||
# 비교 결과
|
||||
comparisons = []
|
||||
|
||||
for current_fw in current_inventory:
|
||||
component_name = current_fw['Name']
|
||||
current_version = current_fw['Version']
|
||||
|
||||
# 최신 버전 찾기 (컴포넌트명 매칭)
|
||||
latest = None
|
||||
for lv in latest_versions:
|
||||
if lv.component_name.lower() in component_name.lower():
|
||||
# 서버 모델 확인
|
||||
if not lv.server_model or lv.server_model == server.model:
|
||||
latest = lv
|
||||
break
|
||||
|
||||
# 비교
|
||||
comparison = FirmwareComparisonResult(
|
||||
component_name=component_name,
|
||||
current_version=current_version,
|
||||
latest_version=latest.latest_version if latest else None
|
||||
)
|
||||
|
||||
result = comparison.to_dict()
|
||||
|
||||
# 추가 정보
|
||||
if latest:
|
||||
result['latest_info'] = {
|
||||
'release_date': latest.release_date,
|
||||
'download_url': latest.download_url,
|
||||
'file_name': latest.file_name,
|
||||
'is_critical': latest.is_critical,
|
||||
'notes': latest.notes
|
||||
}
|
||||
|
||||
comparisons.append(result)
|
||||
|
||||
# 통계
|
||||
total = len(comparisons)
|
||||
outdated = len([c for c in comparisons if c['status'] == 'outdated'])
|
||||
latest_count = len([c for c in comparisons if c['status'] == 'latest'])
|
||||
unknown = len([c for c in comparisons if c['status'] == 'unknown'])
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'server': {
|
||||
'id': server.id,
|
||||
'name': server.name,
|
||||
'model': server.model
|
||||
},
|
||||
'comparisons': comparisons,
|
||||
'summary': {
|
||||
'total': total,
|
||||
'outdated': outdated,
|
||||
'latest': latest_count,
|
||||
'unknown': unknown
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'오류: {str(e)}'
|
||||
})
|
||||
|
||||
@idrac_bp.route('/api/servers/firmware/compare-multi', methods=['POST'])
|
||||
def compare_multi_servers_firmware():
|
||||
"""
|
||||
여러 서버의 펌웨어 버전 비교
|
||||
"""
|
||||
try:
|
||||
data = request.json
|
||||
server_ids = data.get('server_ids', [])
|
||||
|
||||
if not server_ids:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '서버를 선택하세요'
|
||||
})
|
||||
|
||||
results = []
|
||||
|
||||
for server_id in server_ids:
|
||||
server = IdracServer.query.get(server_id)
|
||||
if not server:
|
||||
continue
|
||||
|
||||
try:
|
||||
# 각 서버 비교
|
||||
client = DellRedfishClient(server.ip_address, server.username, server.password)
|
||||
current_inventory = client.get_firmware_inventory()
|
||||
|
||||
latest_versions = FirmwareVersion.query.filter_by(is_active=True).all()
|
||||
|
||||
outdated_count = 0
|
||||
outdated_items = []
|
||||
|
||||
for current_fw in current_inventory:
|
||||
component_name = current_fw['Name']
|
||||
current_version = current_fw['Version']
|
||||
|
||||
# 최신 버전 찾기
|
||||
for lv in latest_versions:
|
||||
if lv.component_name.lower() in component_name.lower():
|
||||
comparison = FirmwareComparisonResult(
|
||||
component_name=component_name,
|
||||
current_version=current_version,
|
||||
latest_version=lv.latest_version
|
||||
)
|
||||
|
||||
if comparison.status == 'outdated':
|
||||
outdated_count += 1
|
||||
outdated_items.append({
|
||||
'component': component_name,
|
||||
'current': current_version,
|
||||
'latest': lv.latest_version
|
||||
})
|
||||
break
|
||||
|
||||
results.append({
|
||||
'server_id': server.id,
|
||||
'server_name': server.name,
|
||||
'outdated_count': outdated_count,
|
||||
'outdated_items': outdated_items,
|
||||
'status': 'needs_update' if outdated_count > 0 else 'up_to_date'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
results.append({
|
||||
'server_id': server.id,
|
||||
'server_name': server.name,
|
||||
'error': str(e)
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'results': results
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'오류: {str(e)}'
|
||||
})
|
||||
|
||||
|
||||
# ========================================
|
||||
# 초기 데이터 생성 함수
|
||||
# ========================================
|
||||
|
||||
def init_firmware_versions():
|
||||
"""초기 펌웨어 버전 데이터 생성"""
|
||||
initial_versions = [
|
||||
{
|
||||
'component_name': 'BIOS',
|
||||
'component_type': 'Firmware',
|
||||
'vendor': 'Dell',
|
||||
'server_model': 'PowerEdge R750',
|
||||
'latest_version': '2.15.0',
|
||||
'release_date': '2024-01-15',
|
||||
'notes': 'PowerEdge R750 최신 BIOS'
|
||||
},
|
||||
{
|
||||
'component_name': 'iDRAC',
|
||||
'component_type': 'Firmware',
|
||||
'vendor': 'Dell',
|
||||
'latest_version': '6.10.30.00',
|
||||
'release_date': '2024-02-20',
|
||||
'notes': 'iDRAC9 최신 펌웨어 (모든 모델 공용)'
|
||||
},
|
||||
{
|
||||
'component_name': 'PERC H755',
|
||||
'component_type': 'Firmware',
|
||||
'vendor': 'Dell',
|
||||
'server_model': 'PowerEdge R750',
|
||||
'latest_version': '25.5.9.0001',
|
||||
'release_date': '2024-01-10',
|
||||
'notes': 'PERC H755 RAID 컨트롤러'
|
||||
},
|
||||
{
|
||||
'component_name': 'BIOS',
|
||||
'component_type': 'Firmware',
|
||||
'vendor': 'Dell',
|
||||
'server_model': 'PowerEdge R640',
|
||||
'latest_version': '2.19.2',
|
||||
'release_date': '2024-02-01',
|
||||
'notes': 'PowerEdge R640 최신 BIOS'
|
||||
},
|
||||
{
|
||||
'component_name': 'CPLD',
|
||||
'component_type': 'Firmware',
|
||||
'vendor': 'Dell',
|
||||
'latest_version': '1.0.6',
|
||||
'release_date': '2023-12-15',
|
||||
'notes': '시스템 보드 CPLD (14G/15G 공용)'
|
||||
},
|
||||
]
|
||||
|
||||
for data in initial_versions:
|
||||
# 중복 체크
|
||||
existing = FirmwareVersion.query.filter_by(
|
||||
component_name=data['component_name'],
|
||||
server_model=data.get('server_model')
|
||||
).first()
|
||||
|
||||
if not existing:
|
||||
version = FirmwareVersion(**data)
|
||||
db.session.add(version)
|
||||
|
||||
try:
|
||||
db.session.commit()
|
||||
print("✓ 초기 펌웨어 버전 데이터 생성 완료")
|
||||
except:
|
||||
db.session.rollback()
|
||||
print("⚠ 초기 데이터 생성 중 오류 (이미 있을 수 있음)")
|
||||
Reference in New Issue
Block a user