790 lines
32 KiB
Python
790 lines
32 KiB
Python
"""
|
|
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()
|
|
|