update
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user