""" 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 backend.models.firmware_version import FirmwareVersion, FirmwareComparisonResult 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: if existing.is_active: # 활성 서버가 이미 있음 return jsonify({ 'success': False, 'message': f'이미 등록된 IP입니다: {data["ip_address"]}' }) else: # 삭제된 서버 재활용! existing.name = data['name'] existing.username = data.get('username', 'root') existing.password = data['password'] existing.group_name = data.get('group_name') existing.location = data.get('location') existing.model = data.get('model') existing.notes = data.get('notes') existing.is_active = True # 다시 활성화! existing.status = 'offline' existing.current_bios = None existing.last_connected = None existing.last_updated = None db.session.commit() return jsonify({ 'success': True, 'message': f'서버 {existing.name} 추가 완료 (기존 데이터 재활용)', 'server': existing.to_dict() }) # 서버 생성 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/', 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/', 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//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//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/', 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() # 초기 펌웨어 버전 데이터 생성 (최초 실행 시) if FirmwareVersion.query.count() == 0: init_firmware_versions() # ======================================== # 펌웨어 버전 비교 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/', 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/', 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//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("⚠ 초기 데이터 생성 중 오류 (이미 있을 수 있음)")