""" 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/', 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//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/', 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/', 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()