This commit is contained in:
unknown
2026-03-21 07:26:38 +09:00
parent 166d94fca4
commit 40150cd66d
406 changed files with 34179 additions and 55658 deletions
+73
View File
@@ -0,0 +1,73 @@
from html.parser import HTMLParser
class StructureValidator(HTMLParser):
def __init__(self):
super().__init__()
self.stack = []
self.important_divs = {
'fileTabsContent': None,
'processed': None,
'repository': None,
'repoTabsContent': None,
'pills-backup': None
}
def handle_starttag(self, tag, attrs):
if tag == 'div':
attr_dict = dict(attrs)
div_id = attr_dict.get('id', '')
div_class = attr_dict.get('class', '')
line_no = self.getpos()[0]
self.stack.append({
'tag': 'div',
'id': div_id,
'class': div_class,
'line': line_no
})
if div_id in self.important_divs:
self.important_divs[div_id] = {
'line': line_no,
'class': div_class,
'depth': len(self.stack)
}
def handle_endtag(self, tag):
if tag == 'div' and self.stack:
self.stack.pop()
def validate(self, html_content):
self.feed(html_content)
print("=== IMPORTANT DIV INFORMATION ===\n")
for div_id, info in self.important_divs.items():
if info:
print(f"{div_id}:")
print(f" Line: {info['line']}")
print(f" Class: {info['class']}")
print(f" Depth: {info['depth']}")
print()
# Check if processed and repository are siblings
if self.important_divs['processed'] and self.important_divs['repository']:
proc_depth = self.important_divs['processed']['depth']
repo_depth = self.important_divs['repository']['depth']
print("=== RELATIONSHIP CHECK ===")
print(f"#processed depth: {proc_depth}")
print(f"#repository depth: {repo_depth}")
if proc_depth == repo_depth:
print("✓ They are SIBLINGS (same depth) - CORRECT")
else:
print("✗ They are NOT siblings (different depth) - PROBLEM!")
print(f" Depth difference: {abs(proc_depth - repo_depth)}")
# Read the file
file_path = r"d:\Code\iDRAC_Info\idrac_info\backend\templates\index.html"
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
validator = StructureValidator()
validator.validate(content)
+66
View File
@@ -0,0 +1,66 @@
from html.parser import HTMLParser
class TagMatcher(HTMLParser):
def __init__(self):
super().__init__()
self.stack = []
self.div_map = {} # line -> info
self.errors = []
def handle_starttag(self, tag, attrs):
if tag == 'div':
attr_dict = dict(attrs)
div_id = attr_dict.get('id', '')
div_class = attr_dict.get('class', '')
line_no = self.getpos()[0]
info = {
'tag': 'div',
'id': div_id,
'class': div_class,
'start_line': line_no,
'parent': self.stack[-1]['id'] if self.stack else None
}
self.stack.append(info)
def handle_endtag(self, tag):
if tag == 'div':
if self.stack:
info = self.stack.pop()
info['end_line'] = self.getpos()[0]
# Store important divs
if info['id'] in ['fileTabsContent', 'processed', 'repository', 'repoTabsContent', 'pills-backup', 'pills-mac', 'pills-guid', 'pills-gpu']:
self.div_map[info['id']] = info
else:
self.errors.append(f"Unexpected closing div at line {self.getpos()[0]}")
def validate(self, html_content):
self.feed(html_content)
print("=== DIV STRUCTURE MAP ===\n")
order = ['fileTabsContent', 'processed', 'repository', 'repoTabsContent', 'pills-backup', 'pills-mac', 'pills-guid', 'pills-gpu']
for div_id in order:
if div_id in self.div_map:
info = self.div_map[div_id]
print(f"ID: {div_id}")
print(f" Start: {info['start_line']}")
print(f" End: {info['end_line']}")
print(f" Parent: {info['parent']}")
print()
else:
print(f"ID: {div_id} - NOT FOUND or NOT CLOSED properly\n")
if self.stack:
print("\n=== UNCLOSED DIVS ===")
for info in self.stack:
print(f"Unclosed div starting at line {info['start_line']} (ID: {info['id']}, Class: {info['class']})")
# Read the file
file_path = r"d:\Code\iDRAC_Info\idrac_info\backend\templates\index.html"
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
matcher = TagMatcher()
matcher.validate(content)
+28
View File
@@ -0,0 +1,28 @@
from html.parser import HTMLParser
class DivStructurePrinter(HTMLParser):
def __init__(self):
super().__init__()
self.stack = []
def handle_starttag(self, tag, attrs):
if tag == 'div':
attr_dict = dict(attrs)
ident = attr_dict.get('id', '')
if ident in ['fileTabsContent', 'processed', 'repository', 'repoTabsContent', 'pills-backup']:
print(f"OPEN {ident:15} at line {self.getpos()[0]} (Stack depth: {len(self.stack)})")
self.stack.append(ident)
def handle_endtag(self, tag):
if tag == 'div':
if self.stack:
ident = self.stack.pop()
if ident in ['fileTabsContent', 'processed', 'repository', 'repoTabsContent', 'pills-backup']:
print(f"CLOSE {ident:15} at line {self.getpos()[0]} (Stack depth: {len(self.stack)})")
with open(r"d:\Code\iDRAC_Info\idrac_info\backend\templates\index.html", "r", encoding="utf-8") as f:
content = f.read()
parser = DivStructurePrinter()
parser.feed(content)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+86
View File
@@ -0,0 +1,86 @@
"""
BIOS Baseline 모델
backend/models/bios_baseline.py
Redfish로 가져온 표준 BIOS 설정을 저장하고 관리하는 모델
"""
from datetime import datetime
from backend.models.user import db
class BiosBaseline(db.Model):
"""BIOS 설정 기준값 모델 - Redfish로 가져온 표준 설정"""
__tablename__ = 'bios_baselines'
id = db.Column(db.Integer, primary_key=True)
# 기본 정보
name = db.Column(db.String(100), nullable=False, unique=True) # 예: "R6615_GPU_Standard_2024"
server_model = db.Column(db.String(100), nullable=False) # 예: "PowerEdge R6615"
server_type = db.Column(db.String(50)) # 예: "GPU Server", "Storage Server"
description = db.Column(db.String(500)) # 설명
# Redfish로 가져온 원본 데이터
bios_data = db.Column(db.JSON, nullable=False) # BIOS 설정값 전체 (JSON)
bios_metadata = db.Column(db.JSON, nullable=True) # BIOS 메타데이터 (AttributeName -> DisplayName 매핑)
idrac_data = db.Column(db.JSON, nullable=True) # iDRAC 설정값 (JSON)
idrac_metadata = db.Column(db.JSON, nullable=True) # iDRAC 메타데이터 (DisplayName 매핑)
raid_data = db.Column(db.JSON, nullable=True) # RAID/Storage 구성 (JSON)
nic_data = db.Column(db.JSON, nullable=True) # NIC 설정 (Adapters)
boot_data = db.Column(db.JSON, nullable=True) # 부팅 설정 (Order)
firmware_data = db.Column(db.JSON, nullable=True) # 펌웨어 목록
# 메타데이터
source_ip = db.Column(db.String(50)) # 이 baseline을 생성한 서버 IP
source_service_tag = db.Column(db.String(50)) # 원본 서버 Service Tag
bios_version = db.Column(db.String(50)) # BIOS 버전
total_settings = db.Column(db.Integer) # 총 설정 항목 수
# 복사 관계 (어떤 baseline에서 복사되었는지)
copied_from_id = db.Column(db.Integer, db.ForeignKey('bios_baselines.id'), nullable=True)
# 상태 및 관리
is_active = db.Column(db.Boolean, default=True)
created_by = db.Column(db.String(100)) # 생성자
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 관계 설정
copied_from = db.relationship('BiosBaseline', remote_side=[id], backref='copies')
def to_dict(self, include_data=False):
"""
딕셔너리로 변환
include_data: True면 bios_data, idrac_data, raid_data 등 상세 데이터 포함 (상세 조회 시)
"""
data = {
'id': self.id,
'name': self.name,
'server_model': self.server_model,
'server_type': self.server_type,
'description': self.description,
'source_ip': self.source_ip,
'source_service_tag': self.source_service_tag,
'bios_version': self.bios_version,
'total_settings': self.total_settings,
'copied_from_id': self.copied_from_id,
'is_active': self.is_active,
'created_by': self.created_by,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
}
if include_data:
data['bios_data'] = self.bios_data
data['bios_metadata'] = self.bios_metadata
data['idrac_data'] = self.idrac_data
data['idrac_metadata'] = self.idrac_metadata
data['raid_data'] = self.raid_data
data['nic_data'] = self.nic_data
data['boot_data'] = self.boot_data
data['firmware_data'] = self.firmware_data
return data
def __repr__(self):
return f'<BiosBaseline {self.name} ({self.server_model})>'
+36
View File
@@ -0,0 +1,36 @@
from datetime import datetime
from backend.models.user import db
class ServerConfigSnapshot(db.Model):
"""
서버 설정 스냅샷 (BIOS, iDRAC, System 등)
"""
__tablename__ = 'server_config_snapshots'
id = db.Column(db.Integer, primary_key=True)
server_id = db.Column(db.Integer, db.ForeignKey('idrac_servers.id'), nullable=False)
# 설정 유형: bios, idrac, system
config_type = db.Column(db.String(20), nullable=False)
# JSON 데이터 저장
data = db.Column(db.JSON, nullable=True)
# 데이터 해시 (변경 감지용 - Optional)
hash_value = db.Column(db.String(64), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 관계 설정 (IdracServer와의 관계는 IdracServer 모델에서 backref로 설정될 수도 있음)
server = db.relationship('IdracServer', backref=db.backref('config_snapshots', lazy=True))
def to_dict(self):
return {
'id': self.id,
'server_id': self.server_id,
'server_name': self.server.name if self.server else 'Unknown',
'config_type': self.config_type,
'data': self.data,
'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S')
}
+14
View File
@@ -12,6 +12,9 @@ from .idrac_routes import register_idrac_routes
from .catalog_sync import catalog_bp
from .scp_routes import scp_bp
from .drm_sync import drm_sync_bp
from .server_config_routes import server_config_bp
from .bios_baseline_routes import register_bios_baseline_routes
from .script_manager import script_manager_bp
def register_routes(app: Flask, socketio=None) -> None:
@@ -28,3 +31,14 @@ def register_routes(app: Flask, socketio=None) -> None:
app.register_blueprint(catalog_bp)
app.register_blueprint(scp_bp)
app.register_blueprint(drm_sync_bp)
app.register_blueprint(server_config_bp)
register_bios_baseline_routes(app)
app.register_blueprint(script_manager_bp)
# CSRF 제외 - server_config API 엔드포인트
try:
csrf = app.extensions.get('csrf')
if csrf:
csrf.exempt(server_config_bp)
except Exception as e:
app.logger.warning(f"CSRF exemption failed for server_config_bp: {e}")
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+789
View File
@@ -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()
+14
View File
@@ -66,7 +66,21 @@ def view_file():
base = Path(Config.BACKUP_FOLDER)
safe_date = Path(date).name if date else ""
target = (base / safe_date / safe_name).resolve()
elif folder == "server_info":
base = Path(Config.BACKUP_FOLDER)
safe_date = Path(date).name if date else ""
target = (base / safe_date / safe_name).resolve()
elif folder == "mac":
base = Path(Config.MAC_FOLDER)
target = (base / safe_name).resolve()
elif folder == "guid":
base = Path(Config.GUID_FOLDER)
target = (base / safe_name).resolve()
elif folder == "gpu":
base = Path(Config.GPU_FOLDER)
target = (base / safe_name).resolve()
else:
# Default: Processed files (Staging)
base = Path(Config.IDRAC_INFO_FOLDER)
target = (base / safe_name).resolve()
File diff suppressed because it is too large Load Diff
-669
View File
@@ -1,669 +0,0 @@
"""
Dell iDRAC 멀티 서버 펌웨어 관리 라우트
backend/routes/idrac_routes.py
- CSRF 보호 제외 추가
"""
from flask import Blueprint, render_template, request, jsonify, current_app
from werkzeug.utils import secure_filename
import os
from datetime import datetime
from backend.services.idrac_redfish_client import DellRedfishClient
from backend.models.idrac_server import IdracServer, db
from flask_socketio import emit
import threading
# Blueprint 생성
idrac_bp = Blueprint('idrac', __name__, url_prefix='/idrac')
# 설정
UPLOAD_FOLDER = 'uploads/firmware'
ALLOWED_EXTENSIONS = {'exe', 'bin'}
MAX_FILE_SIZE = 500 * 1024 * 1024 # 500MB
def allowed_file(filename):
"""허용된 파일 형식 확인"""
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
# ========================================
# 메인 페이지
# ========================================
@idrac_bp.route('/')
def index():
"""iDRAC 멀티 서버 관리 메인 페이지"""
return render_template('idrac_firmware.html')
# ========================================
# 서버 관리 API
# ========================================
@idrac_bp.route('/api/servers', methods=['GET'])
def get_servers():
"""등록된 서버 목록 조회"""
try:
group = request.args.get('group') # 그룹 필터
query = IdracServer.query.filter_by(is_active=True)
if group and group != 'all':
query = query.filter_by(group_name=group)
servers = query.order_by(IdracServer.name).all()
return jsonify({
'success': True,
'servers': [s.to_dict() for s in servers]
})
except Exception as e:
return jsonify({
'success': False,
'message': f'오류: {str(e)}'
})
@idrac_bp.route('/api/servers', methods=['POST'])
def add_server():
"""서버 추가"""
try:
data = request.json
# 필수 필드 확인
if not all([data.get('name'), data.get('ip_address'), data.get('password')]):
return jsonify({
'success': False,
'message': '필수 필드를 모두 입력하세요 (서버명, IP, 비밀번호)'
})
# 중복 IP 확인
existing = IdracServer.query.filter_by(ip_address=data['ip_address']).first()
if existing:
return jsonify({
'success': False,
'message': f'이미 등록된 IP입니다: {data["ip_address"]}'
})
# 서버 생성
server = IdracServer(
name=data['name'],
ip_address=data['ip_address'],
username=data.get('username', 'root'),
password=data['password'],
group_name=data.get('group_name'),
location=data.get('location'),
model=data.get('model'),
notes=data.get('notes')
)
db.session.add(server)
db.session.commit()
return jsonify({
'success': True,
'message': f'서버 {server.name} 추가 완료',
'server': server.to_dict()
})
except Exception as e:
db.session.rollback()
return jsonify({
'success': False,
'message': f'오류: {str(e)}'
})
@idrac_bp.route('/api/servers/<int:server_id>', methods=['PUT'])
def update_server(server_id):
"""서버 정보 수정"""
try:
server = IdracServer.query.get(server_id)
if not server:
return jsonify({
'success': False,
'message': '서버를 찾을 수 없습니다'
})
data = request.json
# 업데이트
if 'name' in data:
server.name = data['name']
if 'ip_address' in data:
server.ip_address = data['ip_address']
if 'username' in data:
server.username = data['username']
if 'password' in data:
server.password = data['password']
if 'group_name' in data:
server.group_name = data['group_name']
if 'location' in data:
server.location = data['location']
if 'model' in data:
server.model = data['model']
if 'notes' in data:
server.notes = data['notes']
db.session.commit()
return jsonify({
'success': True,
'message': '서버 정보 수정 완료',
'server': server.to_dict()
})
except Exception as e:
db.session.rollback()
return jsonify({
'success': False,
'message': f'오류: {str(e)}'
})
@idrac_bp.route('/api/servers/<int:server_id>', methods=['DELETE'])
def delete_server(server_id):
"""서버 삭제 (소프트 삭제)"""
try:
server = IdracServer.query.get(server_id)
if not server:
return jsonify({
'success': False,
'message': '서버를 찾을 수 없습니다'
})
server.is_active = False
db.session.commit()
return jsonify({
'success': True,
'message': f'서버 {server.name} 삭제 완료'
})
except Exception as e:
db.session.rollback()
return jsonify({
'success': False,
'message': f'오류: {str(e)}'
})
@idrac_bp.route('/api/groups', methods=['GET'])
def get_groups():
"""등록된 그룹 목록"""
try:
groups = db.session.query(IdracServer.group_name)\
.filter(IdracServer.is_active == True)\
.filter(IdracServer.group_name.isnot(None))\
.distinct()\
.all()
group_list = [g[0] for g in groups if g[0]]
return jsonify({
'success': True,
'groups': group_list
})
except Exception as e:
return jsonify({
'success': False,
'message': f'오류: {str(e)}'
})
# ========================================
# 연결 및 상태 확인 API
# ========================================
@idrac_bp.route('/api/servers/<int:server_id>/test', methods=['POST'])
def test_connection(server_id):
"""단일 서버 연결 테스트"""
try:
server = IdracServer.query.get(server_id)
if not server:
return jsonify({
'success': False,
'message': '서버를 찾을 수 없습니다'
})
client = DellRedfishClient(server.ip_address, server.username, server.password)
if client.check_connection():
server.status = 'online'
server.last_connected = datetime.utcnow()
db.session.commit()
return jsonify({
'success': True,
'message': f'{server.name} 연결 성공'
})
else:
server.status = 'offline'
db.session.commit()
return jsonify({
'success': False,
'message': f'{server.name} 연결 실패'
})
except Exception as e:
return jsonify({
'success': False,
'message': f'오류: {str(e)}'
})
@idrac_bp.route('/api/servers/test-multi', methods=['POST'])
def test_connections_multi():
"""다중 서버 일괄 연결 테스트"""
try:
data = request.json
server_ids = data.get('server_ids', [])
if not server_ids:
return jsonify({
'success': False,
'message': '서버를 선택하세요'
})
results = []
for server_id in server_ids:
server = IdracServer.query.get(server_id)
if not server:
continue
try:
client = DellRedfishClient(server.ip_address, server.username, server.password)
if client.check_connection():
server.status = 'online'
server.last_connected = datetime.utcnow()
results.append({
'server_id': server.id,
'server_name': server.name,
'success': True,
'message': '연결 성공'
})
else:
server.status = 'offline'
results.append({
'server_id': server.id,
'server_name': server.name,
'success': False,
'message': '연결 실패'
})
except Exception as e:
server.status = 'offline'
results.append({
'server_id': server.id,
'server_name': server.name,
'success': False,
'message': str(e)
})
db.session.commit()
success_count = sum(1 for r in results if r['success'])
return jsonify({
'success': True,
'results': results,
'summary': {
'total': len(results),
'success': success_count,
'failed': len(results) - success_count
}
})
except Exception as e:
return jsonify({
'success': False,
'message': f'오류: {str(e)}'
})
@idrac_bp.route('/api/servers/<int:server_id>/firmware', methods=['GET'])
def get_server_firmware(server_id):
"""단일 서버 펌웨어 버전 조회"""
try:
server = IdracServer.query.get(server_id)
if not server:
return jsonify({
'success': False,
'message': '서버를 찾을 수 없습니다'
})
client = DellRedfishClient(server.ip_address, server.username, server.password)
inventory = client.get_firmware_inventory()
# BIOS 버전 업데이트
for item in inventory:
if 'BIOS' in item.get('Name', ''):
server.current_bios = item.get('Version')
break
db.session.commit()
return jsonify({
'success': True,
'data': inventory
})
except Exception as e:
return jsonify({
'success': False,
'message': f'오류: {str(e)}'
})
# ========================================
# 멀티 서버 펌웨어 업로드 API
# ========================================
@idrac_bp.route('/api/upload-multi', methods=['POST'])
def upload_multi():
"""다중 서버에 펌웨어 일괄 업로드"""
try:
if 'file' not in request.files:
return jsonify({
'success': False,
'message': '파일이 없습니다'
})
file = request.files['file']
server_ids_str = request.form.get('server_ids')
if not server_ids_str:
return jsonify({
'success': False,
'message': '서버를 선택하세요'
})
server_ids = [int(x) for x in server_ids_str.split(',')]
if file.filename == '':
return jsonify({
'success': False,
'message': '파일이 선택되지 않았습니다'
})
if not allowed_file(file.filename):
return jsonify({
'success': False,
'message': '허용되지 않은 파일 형식입니다 (.exe, .bin만 가능)'
})
# 로컬에 임시 저장
filename = secure_filename(file.filename)
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
local_path = os.path.join(UPLOAD_FOLDER, filename)
file.save(local_path)
# 백그라운드 스레드로 업로드 시작
from backend.services import watchdog_handler
socketio = watchdog_handler.socketio
def upload_to_servers():
results = []
for idx, server_id in enumerate(server_ids):
server = IdracServer.query.get(server_id)
if not server:
continue
try:
# 진행 상황 전송
if socketio:
socketio.emit('upload_progress', {
'server_id': server.id,
'server_name': server.name,
'status': 'uploading',
'progress': 0,
'message': '업로드 시작...'
})
server.status = 'updating'
db.session.commit()
client = DellRedfishClient(server.ip_address, server.username, server.password)
result = client.upload_firmware_staged(local_path)
if result['success']:
server.status = 'online'
server.last_updated = datetime.utcnow()
results.append({
'server_id': server.id,
'server_name': server.name,
'success': True,
'job_id': result['job_id'],
'message': '업로드 완료'
})
if socketio:
socketio.emit('upload_progress', {
'server_id': server.id,
'server_name': server.name,
'status': 'completed',
'progress': 100,
'message': '업로드 완료',
'job_id': result['job_id']
})
else:
server.status = 'online'
results.append({
'server_id': server.id,
'server_name': server.name,
'success': False,
'message': result.get('message', '업로드 실패')
})
if socketio:
socketio.emit('upload_progress', {
'server_id': server.id,
'server_name': server.name,
'status': 'failed',
'progress': 0,
'message': result.get('message', '업로드 실패')
})
db.session.commit()
except Exception as e:
server.status = 'online'
db.session.commit()
results.append({
'server_id': server.id,
'server_name': server.name,
'success': False,
'message': str(e)
})
if socketio:
socketio.emit('upload_progress', {
'server_id': server.id,
'server_name': server.name,
'status': 'failed',
'progress': 0,
'message': str(e)
})
# 최종 결과 전송
if socketio:
success_count = sum(1 for r in results if r['success'])
socketio.emit('upload_complete', {
'results': results,
'summary': {
'total': len(results),
'success': success_count,
'failed': len(results) - success_count
}
})
# 스레드 시작
thread = threading.Thread(target=upload_to_servers)
thread.daemon = True
thread.start()
return jsonify({
'success': True,
'message': f'{len(server_ids)}대 서버에 업로드 시작',
'filename': filename
})
except Exception as e:
return jsonify({
'success': False,
'message': f'업로드 오류: {str(e)}'
})
# ========================================
# 기존 단일 서버 API (호환성 유지)
# ========================================
@idrac_bp.route('/api/files/local', methods=['GET'])
def list_local_files():
"""로컬에 저장된 DUP 파일 목록"""
try:
files = []
if os.path.exists(UPLOAD_FOLDER):
for filename in os.listdir(UPLOAD_FOLDER):
filepath = os.path.join(UPLOAD_FOLDER, filename)
if os.path.isfile(filepath):
file_size = os.path.getsize(filepath)
files.append({
'name': filename,
'size': file_size,
'size_mb': round(file_size / (1024 * 1024), 2),
'uploaded_at': datetime.fromtimestamp(
os.path.getmtime(filepath)
).strftime('%Y-%m-%d %H:%M:%S')
})
return jsonify({
'success': True,
'files': files
})
except Exception as e:
return jsonify({
'success': False,
'message': f'오류: {str(e)}'
})
@idrac_bp.route('/api/files/local/<filename>', methods=['DELETE'])
def delete_local_file(filename):
"""로컬 DUP 파일 삭제"""
try:
filepath = os.path.join(UPLOAD_FOLDER, secure_filename(filename))
if os.path.exists(filepath):
os.remove(filepath)
return jsonify({
'success': True,
'message': f'{filename} 삭제 완료'
})
else:
return jsonify({
'success': False,
'message': '파일을 찾을 수 없습니다'
})
except Exception as e:
return jsonify({
'success': False,
'message': f'삭제 오류: {str(e)}'
})
# ========================================
# 서버 재부팅 API (멀티)
# ========================================
@idrac_bp.route('/api/servers/reboot-multi', methods=['POST'])
def reboot_servers_multi():
"""선택한 서버들 일괄 재부팅"""
try:
data = request.json
server_ids = data.get('server_ids', [])
reboot_type = data.get('type', 'GracefulRestart')
if not server_ids:
return jsonify({
'success': False,
'message': '서버를 선택하세요'
})
results = []
for server_id in server_ids:
server = IdracServer.query.get(server_id)
if not server:
continue
try:
client = DellRedfishClient(server.ip_address, server.username, server.password)
result = client.reboot_server(reboot_type)
if result:
results.append({
'server_id': server.id,
'server_name': server.name,
'success': True,
'message': '재부팅 시작'
})
else:
results.append({
'server_id': server.id,
'server_name': server.name,
'success': False,
'message': '재부팅 실패'
})
except Exception as e:
results.append({
'server_id': server.id,
'server_name': server.name,
'success': False,
'message': str(e)
})
success_count = sum(1 for r in results if r['success'])
return jsonify({
'success': True,
'results': results,
'summary': {
'total': len(results),
'success': success_count,
'failed': len(results) - success_count
}
})
except Exception as e:
return jsonify({
'success': False,
'message': f'오류: {str(e)}'
})
# Blueprint 등록 함수
def register_idrac_routes(app):
"""
iDRAC 멀티 서버 관리 Blueprint 등록
Args:
app: Flask 애플리케이션 인스턴스
"""
# uploads 디렉토리 생성
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
# Blueprint 등록
app.register_blueprint(idrac_bp)
# CSRF 보호 제외 - iDRAC API 엔드포인트
try:
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect()
# API 엔드포인트는 CSRF 검증 제외
csrf.exempt(idrac_bp)
except:
pass # CSRF 설정 실패해도 계속 진행
# DB 테이블 생성
with app.app_context():
db.create_all()
+155 -18
View File
@@ -98,7 +98,7 @@ def index():
end_page = min(start_page + 9, total_pages)
# 4. 백업 폴더 목록
backup_dirs = [d for d in backup_dir.iterdir() if d.is_dir()]
backup_dirs = [d for d in backup_dir.iterdir() if d.is_dir() and d.name != "archive"]
backup_dirs.sort(key=lambda p: p.stat().st_mtime, reverse=True)
backup_page = int(request.args.get("backup_page", 1))
@@ -119,31 +119,109 @@ def index():
total_pages=total_pages,
start_page=start_page,
end_page=end_page,
backup_files=backup_files,
backup_files={}, # [DISABLED] Use server_info_files instead
total_backup_pages=total_backup_pages,
backup_page=backup_page,
server_info_files=backup_files, # Server Info (Paginated)
scripts=all_scripts, # 기존 리스트 호환
grouped_scripts=grouped_scripts_sorted, # 카테고리별 분류
xml_files=xml_files,
# [NEW] Repositories
mac_files=_get_file_info_list(Config.MAC_FOLDER),
guid_files=_get_file_info_list(Config.GUID_FOLDER),
gpu_files=_get_file_info_list(Config.GPU_FOLDER),
)
def _get_backup_info(folder_path):
"""
폴더 내의 하위 폴더들을 순회하며 파일 목록 정보를 반환 (백업/아카이브 용)
"""
path = Path(folder_path)
if not path.exists():
return {}
dirs = [d for d in path.iterdir() if d.is_dir() and d.name != "archive"]
dirs.sort(key=lambda p: p.stat().st_mtime, reverse=True)
result = {}
for d in dirs:
files = [f.name for f in d.iterdir() if f.is_file()]
result[d.name] = {"files": files, "count": len(files)}
return result
def _get_file_info_list(folder_path_str):
"""
폴더 내 파일들의 상세 정보(이름, 크기, 수정일)를 리스트로 반환
"""
path = Path(folder_path_str)
if not path.exists():
return []
files = []
try:
for f in path.glob("*"):
if f.is_file():
stat = f.stat()
files.append({
"name": f.name,
"size": stat.st_size,
"mtime": stat.st_mtime, # timestamp for sorting
"date": time.strftime('%Y-%m-%d %H:%M', time.localtime(stat.st_mtime))
})
except Exception as e:
logging.error(f"Error reading folder {folder_path_str}: {e}")
return []
# 기본 정렬: 최신순
return sorted(files, key=lambda x: x['mtime'], reverse=True)
@main_bp.route("/process_ips", methods=["POST"])
@login_required
def process_ips():
ips = request.form.get("ips")
selected_script = request.form.get("script")
selected_xml_file = request.form.get("xmlFile")
if not ips or not selected_script:
return jsonify({"error": "IP 주소와 스크립트를 모두 입력하세요."}), 400
job_type = request.form.get("job_type")
# 변수 초기화
selected_script = ""
profile_name = None
xml_file_path = None
if selected_script == "02-set_config.py" and selected_xml_file:
xml_path = Path(Config.XML_FOLDER) / selected_xml_file
if not xml_path.exists():
return jsonify({"error": "선택한 XML 파일이 존재하지 않습니다."}), 400
xml_file_path = str(xml_path)
slot_priority = request.form.get("slot_priority") # [NEW] 슬롯 우선순위
# 작업 유형에 따른 스크립트 및 프로파일 설정
if job_type in ["mac", "server_info", "guid", "gpu"]:
profile_name = request.form.get("profile")
if job_type in ["mac", "guid"] and not profile_name:
return jsonify({"error": "프로파일을 선택하세요."}), 400
if job_type == "mac":
selected_script = "unified/collect_mac.py"
elif job_type == "server_info":
selected_script = "unified/collect_server_info.py"
elif job_type == "guid":
selected_script = "unified/collect_guid.py"
elif job_type == "gpu":
selected_script = "unified/collect_gpu.py"
profile_name = None # GPU는 프로파일 불필요
else:
# Legacy 모드 (기존 스크립트 선택)
selected_script = request.form.get("script")
selected_xml_file = request.form.get("xmlFile")
if not selected_script:
return jsonify({"error": "스크립트를 선택하세요."}), 400
if selected_script == "02-set_config.py" and selected_xml_file:
xml_path = Path(Config.XML_FOLDER) / selected_xml_file
if not xml_path.exists():
return jsonify({"error": "선택한 XML 파일이 존재하지 않습니다."}), 400
xml_file_path = str(xml_path)
if not ips:
return jsonify({"error": "IP 주소를 입력하세요."}), 400
job_id = str(time.time())
session["job_id"] = job_id
@@ -157,11 +235,11 @@ def process_ips():
observer.start()
future = executor.submit(
process_ips_concurrently, ip_files, job_id, observer, selected_script, xml_file_path
process_ips_concurrently, ip_files, job_id, observer, selected_script, xml_file_path, profile_name, slot_priority
)
future.add_done_callback(lambda x: on_complete(job_id))
logging.info(f"[AJAX] 작업 시작: {job_id}, script: {selected_script}")
logging.info(f"[AJAX] 작업 시작: {job_id}, type: {job_type}, script: {selected_script}, profile: {profile_name}")
return jsonify({"job_id": job_id})
@@ -220,11 +298,21 @@ def delete_file(filename: str):
@main_bp.route("/download_zip", methods=["POST"])
@login_required
def download_zip():
zip_filename = request.form.get("zip_filename", "export")
zip_filename = request.form.get("zip_filename", "export").strip()
zip_path = Path(Config.TEMP_ZIP_FOLDER) / f"{zip_filename}.zip"
# 기본 대상은 스테이징 폴더 (IDRAC_INFO_FOLDER)
target_dir = Path(Config.IDRAC_INFO_FOLDER)
# 입력된 이름이 백업 폴더에 존재하는지 확인
if zip_filename:
possible_backup_path = Path(Config.BACKUP_FOLDER) / zip_filename
if possible_backup_path.exists() and possible_backup_path.is_dir():
target_dir = possible_backup_path
logging.info(f"백업 폴더 ZIP 다운로드 요청: {zip_filename}")
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zipf:
for file in Path(Config.IDRAC_INFO_FOLDER).glob("*"):
for file in target_dir.glob("*"):
if file.is_file():
zipf.write(file, arcname=file.name)
@@ -280,4 +368,53 @@ def move_backup_files():
except Exception as e:
logging.error(f"파일 이동 실패: {e}")
return jsonify({"success": False, "message": f"이동 중 오류 발생: {str(e)}"}), 500
return jsonify({"success": False, "message": f"이동 중 오류 발생: {str(e)}"}), 500
@main_bp.route("/delete_backup_folder/<folder_name>", methods=["POST"])
@login_required
def delete_backup_folder(folder_name: str):
folder_path = Path(Config.BACKUP_FOLDER) / folder_name
# 기본 폴더 보호
if folder_name == "archive":
flash("이 폴더는 삭제할 수 없습니다.", "warning")
return redirect(url_for("main.index"))
if folder_path.exists() and folder_path.is_dir():
try:
shutil.rmtree(folder_path)
flash(f"백업 폴더 '{folder_name}'가 삭제되었습니다.", "success")
logging.info(f"백업 폴더 삭제됨: {folder_name}")
except Exception as e:
logging.error(f"백업 폴더 삭제 오류: {e}")
flash("폴더 삭제 중 오류가 발생했습니다.", "danger")
else:
flash("폴더가 존재하지 않습니다.", "warning")
return redirect(url_for("main.index"))
@main_bp.route("/archive_backup_folder/<folder_name>", methods=["POST"])
@login_required
def archive_backup_folder(folder_name: str):
src_path = Path(Config.BACKUP_FOLDER) / folder_name
archive_root = Path(Config.ARCHIVE_FOLDER)
archive_root.mkdir(parents=True, exist_ok=True)
dst_path = archive_root / folder_name
if src_path.exists() and src_path.is_dir():
try:
# 이름 중복 시 처리 (timestamp 추가)
if dst_path.exists():
timestamp = int(time.time())
dst_path = archive_root / f"{folder_name}_{timestamp}"
shutil.move(str(src_path), str(dst_path))
flash(f"'{folder_name}' 폴더가 아카이브(archive)로 이동되었습니다.", "success")
logging.info(f"백업 아카이빙 성공: {folder_name} -> {dst_path}")
except Exception as e:
logging.error(f"백업 아카이빙 오류: {e}")
flash("폴더 이동 중 오류가 발생했습니다.", "danger")
else:
flash("폴더가 존재하지 않습니다.", "warning")
return redirect(url_for("main.index"))
+331
View File
@@ -0,0 +1,331 @@
"""
스크립트 관리 라우트 및 API
"""
from flask import Blueprint, render_template, request, jsonify, send_file
from flask_login import login_required
from pathlib import Path
import yaml
import json
import logging
script_manager_bp = Blueprint("script_manager", __name__)
# 프로파일 디렉토리
PROFILES_DIR = Path(__file__).parent.parent.parent / "data" / "scripts" / "profiles"
UNIFIED_DIR = Path(__file__).parent.parent.parent / "data" / "scripts" / "unified"
@script_manager_bp.route("/script_manager")
@login_required
def index():
"""스크립트 관리 메인 페이지"""
return render_template("script_manager.html")
@script_manager_bp.route("/api/profiles", methods=["GET"])
@login_required
def get_profiles():
"""모든 프로파일 목록 (카드 뷰용)"""
profiles = []
for yaml_file in PROFILES_DIR.glob("*.yaml"):
try:
with open(yaml_file, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
category = yaml_file.stem.replace('_profiles', '') # mac, guid, server_info
for name, config in data.get('profiles', {}).items():
profiles.append({
"name": name,
"description": config.get("description", ""),
"category": category,
"items": _count_items(config, category),
"is_default": name == "default"
})
except Exception as e:
logging.error(f"프로파일 로드 오류 ({yaml_file}): {e}")
return jsonify(profiles)
@script_manager_bp.route("/api/profiles/<category>", methods=["GET"])
@login_required
def get_profiles_by_category(category):
"""특정 카테고리의 프로파일 목록"""
profiles = []
# 파일명 매핑 (server_info -> server_info_profiles.yaml)
# category가 mac, guid, server_info 등으로 넘어옴
yaml_file = PROFILES_DIR / f"{category}_profiles.yaml"
if not yaml_file.exists():
return jsonify({"error": f"프로파일 파일이 없습니다: {category}"}), 404
try:
with open(yaml_file, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
for name, config in data.get('profiles', {}).items():
profiles.append({
"name": name,
"description": config.get("description", ""),
"category": category,
"items": _count_items(config, category),
"is_default": name == "default"
})
except Exception as e:
logging.error(f"프로파일 로드 오류 ({yaml_file}): {e}")
return jsonify({"error": str(e)}), 500
return jsonify(profiles)
@script_manager_bp.route("/api/profiles/<category>/<name>", methods=["GET"])
@login_required
def get_profile(category, name):
"""특정 프로파일 상세 정보 (편집용)"""
profile_file = PROFILES_DIR / f"{category}_profiles.yaml"
if not profile_file.exists():
return jsonify({"error": "프로파일 파일 없음"}), 404
try:
with open(profile_file, 'r', encoding='utf-8') as f:
profiles = yaml.safe_load(f)['profiles']
profile = profiles.get(name)
if not profile:
return jsonify({"error": "프로파일 없음"}), 404
# UI 친화적 형식으로 변환
return jsonify({
"name": name,
"description": profile.get("description", ""),
"category": category,
"config": profile
})
except Exception as e:
logging.error(f"프로파일 로드 오류: {e}")
return jsonify({"error": str(e)}), 500
@script_manager_bp.route("/api/profiles/<category>/<name>", methods=["PUT"])
@login_required
def update_profile(category, name):
"""프로파일 업데이트"""
profile_file = PROFILES_DIR / f"{category}_profiles.yaml"
if not profile_file.exists():
return jsonify({"error": "프로파일 파일 없음"}), 404
try:
data = request.json
# 기존 프로파일 로드
with open(profile_file, 'r', encoding='utf-8') as f:
profiles_data = yaml.safe_load(f)
# 업데이트
profiles_data['profiles'][name] = data.get('config', {})
# 저장
with open(profile_file, 'w', encoding='utf-8') as f:
yaml.dump(profiles_data, f, allow_unicode=True, default_flow_style=False)
return jsonify({"success": True, "message": "프로파일이 저장되었습니다."})
except Exception as e:
logging.error(f"프로파일 저장 오류: {e}")
return jsonify({"error": str(e)}), 500
@script_manager_bp.route("/api/profiles/<category>", methods=["POST"])
@login_required
def create_profile(category):
"""새 프로파일 생성"""
profile_file = PROFILES_DIR / f"{category}_profiles.yaml"
if not profile_file.exists():
return jsonify({"error": "프로파일 파일 없음"}), 404
try:
data = request.json
name = data.get('name')
if not name:
return jsonify({"error": "프로파일 이름 필요"}), 400
# 기존 프로파일 로드
with open(profile_file, 'r', encoding='utf-8') as f:
profiles_data = yaml.safe_load(f)
# 중복 체크
if name in profiles_data['profiles']:
return jsonify({"error": "이미 존재하는 프로파일"}), 400
# 새 프로파일 추가
profiles_data['profiles'][name] = data.get('config', {})
# 저장
with open(profile_file, 'w', encoding='utf-8') as f:
yaml.dump(profiles_data, f, allow_unicode=True, default_flow_style=False)
return jsonify({"success": True, "message": "프로파일이 생성되었습니다."})
except Exception as e:
logging.error(f"프로파일 생성 오류: {e}")
return jsonify({"error": str(e)}), 500
@script_manager_bp.route("/api/profiles/<category>/<name>", methods=["DELETE"])
@login_required
def delete_profile(category, name):
"""프로파일 삭제"""
profile_file = PROFILES_DIR / f"{category}_profiles.yaml"
if not profile_file.exists():
return jsonify({"error": "프로파일 파일 없음"}), 404
try:
# default 프로파일은 삭제 불가
if name == "default":
return jsonify({"error": "기본 프로파일은 삭제할 수 없습니다."}), 400
# 기존 프로파일 로드
with open(profile_file, 'r', encoding='utf-8') as f:
profiles_data = yaml.safe_load(f)
# 삭제
if name in profiles_data['profiles']:
del profiles_data['profiles'][name]
else:
return jsonify({"error": "프로파일 없음"}), 404
# 저장
with open(profile_file, 'w', encoding='utf-8') as f:
yaml.dump(profiles_data, f, allow_unicode=True, default_flow_style=False)
return jsonify({"success": True, "message": "프로파일이 삭제되었습니다."})
except Exception as e:
logging.error(f"프로파일 삭제 오류: {e}")
return jsonify({"error": str(e)}), 500
@script_manager_bp.route("/api/profiles/<category>/<name>/export", methods=["GET"])
@login_required
def export_profile(category, name):
"""프로파일 YAML 파일 다운로드"""
profile_file = PROFILES_DIR / f"{category}_profiles.yaml"
if not profile_file.exists():
return jsonify({"error": "프로파일 파일 없음"}), 404
try:
with open(profile_file, 'r', encoding='utf-8') as f:
profiles = yaml.safe_load(f)
# 단일 프로파일만 추출
single_profile = {
"profiles": {
name: profiles['profiles'][name]
}
}
# 임시 파일 생성
temp_dir = Path(__file__).parent.parent.parent / "data" / "temp_zips"
temp_dir.mkdir(parents=True, exist_ok=True)
export_file = temp_dir / f"{name}_profile.yaml"
with open(export_file, 'w', encoding='utf-8') as f:
yaml.dump(single_profile, f, allow_unicode=True)
return send_file(export_file, as_attachment=True, download_name=f"{name}_profile.yaml")
except Exception as e:
logging.error(f"프로파일 내보내기 오류: {e}")
return jsonify({"error": str(e)}), 500
return send_file(export_file, as_attachment=True, download_name=f"{name}_profile.yaml")
except Exception as e:
logging.error(f"프로파일 내보내기 오류: {e}")
return jsonify({"error": str(e)}), 500
@script_manager_bp.route("/api/test_racadm", methods=["POST"])
@login_required
def test_racadm():
"""Racadm 명령어 테스트"""
try:
import subprocess
data = request.json
ip = data.get("ip")
user = data.get("user", "root")
password = data.get("password")
command = data.get("command")
if not all([ip, user, password, command]):
return jsonify({"success": False, "error": "필수 입력값 누락"}), 400
# 명령어 구성 (racadm -r <IP> -u <USER> -p <PW> <CMD>)
# 보안을 위해 shell=False로 리스트 형태로 전달
full_cmd = ["racadm", "-r", ip, "-u", user, "-p", password] + command.split()
logging.info(f"[Test] Executing racadm on {ip}: {command}")
# 타임아웃 30초
result = subprocess.run(
full_cmd,
capture_output=True,
text=True,
timeout=30,
check=False
)
return jsonify({
"success": True,
"return_code": result.returncode,
"output": result.stdout + (result.stderr if result.stderr else "")
})
except subprocess.TimeoutExpired:
return jsonify({
"success": False,
"error": "명령어 실행 시간 초과 (30초)"
})
except Exception as e:
logging.error(f"[Test] Racadm execution error: {e}")
return jsonify({"success": False, "error": str(e)})
return jsonify({"success": False, "error": str(e)})
def _count_items(profile: dict, category: str) -> int:
"""프로파일에서 수집할 항목 개수 계산"""
count = 0
if category == "mac":
count += len(profile.get("nic_patterns", []))
if profile.get("infiniband_slots"):
count += len(profile["infiniband_slots"])
if profile.get("include_idrac_mac"):
count += 1
elif category == "guid":
count += len(profile.get("slot_priority", []))
elif category == "gpu":
# GUID slot priority + GPU settings
count += len(profile.get("slot_priority", []))
if profile.get("gpu_settings"):
count += 1
elif category == "server_info":
count += len(profile.get("firmware", []))
count += len(profile.get("bios_settings", []))
count += len(profile.get("idrac_settings", []))
count += len(profile.get("raid_settings", []))
count += len(profile.get("custom_items", []))
return count
def register_script_manager(app):
"""블루프린트 등록"""
app.register_blueprint(script_manager_bp)
+185
View File
@@ -0,0 +1,185 @@
from flask import Blueprint, render_template, request, jsonify
from backend.models.user import db
from backend.models.idrac_server import IdracServer
from backend.models.server_config import ServerConfigSnapshot
from backend.services.idrac_redfish_client import DellRedfishClient
import concurrent.futures
import json
import hashlib
from datetime import datetime
server_config_bp = Blueprint('server_config', __name__, url_prefix='/server-config')
@server_config_bp.route('/')
def index():
"""서버 설정 분석 및 비교 페이지"""
return render_template('server_config.html')
def calculate_checksum(data):
"""JSON 데이터의 SHA256 체크섬 계산"""
json_str = json.dumps(data, sort_keys=True)
return hashlib.sha256(json_str.encode('utf-8')).hexdigest()
@server_config_bp.route('/api/sync', methods=['POST'])
def sync_configs():
"""
선택한 서버들의 설정을 iDRAC에서 가져와 DB에 저장 (동기화)
"""
try:
data = request.json
server_ids = data.get('server_ids', [])
categories = data.get('categories', ['bios', 'idrac', 'system'])
if not server_ids:
return jsonify({'success': False, 'message': '서버를 선택해주세요.'})
results = {'success': [], 'failed': []}
def process_server(server_id):
server = IdracServer.query.get(server_id)
if not server:
return {'id': server_id, 'status': 'failed', 'message': 'Server not found'}
client = DellRedfishClient(server.ip_address, server.username, server.password)
if not client.check_connection():
return {'id': server.id, 'name': server.name, 'status': 'failed', 'message': 'Connection failed'}
sync_results = []
# System Info
if 'system' in categories:
sys_data = client.get_system_info_detailed()
if sys_data:
save_snapshot(server.id, 'system', sys_data)
sync_results.append('system')
# BIOS Attributes
if 'bios' in categories:
bios_data = client.get_bios_attributes_all()
if bios_data:
save_snapshot(server.id, 'bios', bios_data)
sync_results.append('bios')
# iDRAC Attributes
if 'idrac' in categories:
idrac_data = client.get_idrac_attributes_all()
if idrac_data:
save_snapshot(server.id, 'idrac', idrac_data)
sync_results.append('idrac')
return {'id': server.id, 'name': server.name, 'status': 'success', 'synced': sync_results}
def save_snapshot(server_id, config_type, data):
# 기존 스냅샷 확인
snapshot = ServerConfigSnapshot.query.filter_by(
server_id=server_id,
config_type=config_type
).first()
new_hash = calculate_checksum(data)
if snapshot:
# 변경사항이 있는 경우에만 업데이트
if snapshot.hash_value != new_hash:
snapshot.data = data
snapshot.hash_value = new_hash
snapshot.updated_at = datetime.utcnow() # 명시적 업데이트 시간 갱신
else:
snapshot = ServerConfigSnapshot(
server_id=server_id,
config_type=config_type,
data=data,
hash_value=new_hash
)
db.session.add(snapshot)
# 트랜잭션은 개별 commit 혹은 묶어서 commit.
# 여기서는 스레드 안전성을 위해 함수 내에서는 session 조작만 하고
# 실제 commit은 메인 스레드나 별도 처리 필요하지만, Flask-SQLAlchemy는 scoped_session이므로
# 각 요청(스레드)마다 세션이 다를 수 있음.
# 하지만 ThreadPoolExecutor 사용 시 app context 문제가 발생할 수 있음.
# safe way: return data -> main thread saves. OR push app context.
# NOTE: For simplicity in this refactor with ThreadPoolExecutor,
# we will handle DB operations inside the thread but need app context.
# See below for implementation adjustment.
return True
# DB 저장을 위해 app context 필요
from flask import current_app
app = current_app._get_current_object()
def process_with_context(server_id):
with app.app_context():
try:
res = process_server(server_id)
db.session.commit() # Commit per server
return res
except Exception as e:
db.session.rollback()
return {'id': server_id, 'status': 'failed', 'message': str(e)}
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
future_to_id = {executor.submit(process_with_context, sid): sid for sid in server_ids}
for future in concurrent.futures.as_completed(future_to_id):
try:
res = future.result()
if res['status'] == 'success':
results['success'].append(res)
else:
results['failed'].append(res)
except Exception as e:
results['failed'].append({'error': str(e)})
return jsonify({
'success': True,
'results': results
})
except Exception as e:
return jsonify({'success': False, 'message': f"Sync Error: {str(e)}"})
@server_config_bp.route('/api/fetch', methods=['POST'])
def fetch_configs():
"""
DB에 저장된 서버 설정 스냅샷 조회
"""
try:
data = request.json
server_ids = data.get('server_ids', [])
categories = data.get('categories', ['bios', 'idrac', 'system'])
if not server_ids:
return jsonify({'success': False, 'message': '서버를 선택해주세요.'})
results = {}
for server_id in server_ids:
server = IdracServer.query.get(server_id)
if not server:
continue
server_data = {'id': server.id, 'name': server.name, 'ip': server.ip_address}
for cat in categories:
snapshot = ServerConfigSnapshot.query.filter_by(
server_id=server_id,
config_type=cat
).first()
if snapshot:
server_data[cat] = snapshot.data
server_data[f'{cat}_updated'] = snapshot.updated_at.isoformat()
else:
server_data[cat] = None
results[server_id] = server_data
return jsonify({
'success': True,
'results': results
})
except Exception as e:
return jsonify({'success': False, 'message': f"Fetch Error: {str(e)}"})
+79 -5
View File
@@ -333,7 +333,7 @@ def update_server_list():
try:
path.write_text(content, encoding="utf-8")
result = subprocess.run(
[sys.executable, str(Path(Config.SERVER_LIST_FOLDER) / "excel.py")],
[sys.executable, str(Path(Config.SERVER_LIST_FOLDER) / "unified_excel_generator.py"), "--mode", "mac"],
capture_output=True,
text=True,
check=True,
@@ -373,7 +373,7 @@ def update_guid_list():
logging.info(f"GUID 슬롯 우선순위: {slot_priority}")
result = subprocess.run(
[sys.executable, str(Path(Config.SERVER_LIST_FOLDER) / "GUIDtxtT0Execl.py")],
[sys.executable, str(Path(Config.SERVER_LIST_FOLDER) / "unified_excel_generator.py"), "--mode", "guid"],
capture_output=True,
text=True,
check=True,
@@ -417,8 +417,8 @@ def update_gpu_list():
# 2) 엑셀 생성 실행 (GPU 프리셋)
cmd = [
sys.executable,
str(script_path),
"--preset", "gpu",
str(Path(Config.SERVER_LIST_FOLDER) / "unified_excel_generator.py"),
"--mode", "gpu",
"--list-file", str(list_path),
]
result = subprocess.run(
@@ -522,4 +522,78 @@ def scan_network():
except Exception as e:
logging.error(f"Scan network fatal error: {e}")
return jsonify({"success": False, "error": f"서버 내부 오류: {str(e)}"}), 500
return jsonify({"success": False, "error": f"서버 내부 오류: {str(e)}"}), 500
@utils_bp.route("/api/system/control", methods=["POST"])
@login_required
def system_control():
"""
시스템 제어 명령 실행 (Power ON/OFF, Log Clear 등)
"""
data = request.get_json(silent=True) or {}
action = data.get("action")
ips = data.get("ips", [])
if not action or not ips:
return jsonify({"success": False, "error": "Action 또는 IP 목록이 없습니다."}), 400
# 통합 스크립트 사용
script_name = "unified/system_control.py"
script_path = Path(Config.SCRIPT_FOLDER) / script_name
if not script_path.exists():
return jsonify({"success": False, "error": f"스크립트 파일을 찾을 수 없습니다: {script_name}"}), 500
# 임시 IP 파일 생성
import tempfile
try:
# data/temp/uploads 폴더 사용
temp_dir = Path(Config.UPLOAD_FOLDER)
temp_dir.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(mode='w+', delete=False, dir=temp_dir, suffix=".txt", encoding='utf-8') as tf:
tf.write("\n".join(ips))
temp_ip_file = tf.name
# 스크립트 실행: python unified/system_control.py <action> <ip_file>
cmd = [sys.executable, str(script_path), action, temp_ip_file]
logging.info(f"[SystemControl] Executing {action} with {len(ips)} IPs (Unified Script)...")
# 타임아웃 넉넉하게 (작업에 따라 다름)
timeout = 600 if action in ["tsr_collect", "tsr_save"] else 300
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=False, # 에러 발생해도 결과 받음
timeout=timeout
)
# 임시 파일 삭제
try:
os.remove(temp_ip_file)
except OSError:
pass
if result.returncode == 0:
logging.info(f"[SystemControl] Success: {result.stdout}")
return jsonify({
"success": True,
"message": f"작업({action})이 성공적으로 완료되었습니다.",
"details": result.stdout
})
else:
logging.error(f"[SystemControl] Failed: {result.stderr}")
return jsonify({
"success": False,
"error": f"스크립트 실행 중 오류가 발생했습니다.\n{result.stderr}"
}), 500
except subprocess.TimeoutExpired:
return jsonify({"success": False, "error": "스크립트 실행 시간이 초과되었습니다."}), 504
except Exception as e:
logging.error(f"[SystemControl] Exception: {e}")
return jsonify({"success": False, "error": str(e)}), 500
-399
View File
@@ -1,399 +0,0 @@
"""
펌웨어 버전 비교 API 코드
idrac_routes.py 파일의 register_idrac_routes 함수 위에 추가하세요
"""
# ========================================
# 펌웨어 버전 관리 API
# ========================================
@idrac_bp.route('/api/firmware-versions', methods=['GET'])
def get_firmware_versions():
"""등록된 최신 펌웨어 버전 목록"""
try:
server_model = request.args.get('model') # 서버 모델 필터
query = FirmwareVersion.query.filter_by(is_active=True)
if server_model:
# 특정 모델 또는 범용
query = query.filter(
(FirmwareVersion.server_model == server_model) |
(FirmwareVersion.server_model == None)
)
versions = query.order_by(FirmwareVersion.component_name).all()
return jsonify({
'success': True,
'versions': [v.to_dict() for v in versions]
})
except Exception as e:
return jsonify({
'success': False,
'message': f'오류: {str(e)}'
})
@idrac_bp.route('/api/firmware-versions', methods=['POST'])
def add_firmware_version():
"""최신 펌웨어 버전 등록"""
try:
data = request.json
# 필수 필드 확인
if not all([data.get('component_name'), data.get('latest_version')]):
return jsonify({
'success': False,
'message': '컴포넌트명과 버전을 입력하세요'
})
# 중복 확인 (같은 컴포넌트, 같은 모델)
existing = FirmwareVersion.query.filter_by(
component_name=data['component_name'],
server_model=data.get('server_model')
).first()
if existing:
return jsonify({
'success': False,
'message': f'이미 등록된 컴포넌트입니다'
})
# 버전 생성
version = FirmwareVersion(
component_name=data['component_name'],
component_type=data.get('component_type'),
vendor=data.get('vendor'),
server_model=data.get('server_model'),
latest_version=data['latest_version'],
release_date=data.get('release_date'),
download_url=data.get('download_url'),
file_name=data.get('file_name'),
file_size_mb=data.get('file_size_mb'),
notes=data.get('notes'),
is_critical=data.get('is_critical', False)
)
db.session.add(version)
db.session.commit()
return jsonify({
'success': True,
'message': f'{version.component_name} 버전 정보 등록 완료',
'version': version.to_dict()
})
except Exception as e:
db.session.rollback()
return jsonify({
'success': False,
'message': f'오류: {str(e)}'
})
@idrac_bp.route('/api/firmware-versions/<int:version_id>', methods=['PUT'])
def update_firmware_version(version_id):
"""펌웨어 버전 정보 수정"""
try:
version = FirmwareVersion.query.get(version_id)
if not version:
return jsonify({
'success': False,
'message': '버전 정보를 찾을 수 없습니다'
})
data = request.json
# 업데이트
if 'component_name' in data:
version.component_name = data['component_name']
if 'latest_version' in data:
version.latest_version = data['latest_version']
if 'release_date' in data:
version.release_date = data['release_date']
if 'download_url' in data:
version.download_url = data['download_url']
if 'file_name' in data:
version.file_name = data['file_name']
if 'notes' in data:
version.notes = data['notes']
if 'is_critical' in data:
version.is_critical = data['is_critical']
db.session.commit()
return jsonify({
'success': True,
'message': '버전 정보 수정 완료',
'version': version.to_dict()
})
except Exception as e:
db.session.rollback()
return jsonify({
'success': False,
'message': f'오류: {str(e)}'
})
@idrac_bp.route('/api/firmware-versions/<int:version_id>', methods=['DELETE'])
def delete_firmware_version(version_id):
"""펌웨어 버전 정보 삭제"""
try:
version = FirmwareVersion.query.get(version_id)
if not version:
return jsonify({
'success': False,
'message': '버전 정보를 찾을 수 없습니다'
})
version.is_active = False
db.session.commit()
return jsonify({
'success': True,
'message': f'{version.component_name} 버전 정보 삭제 완료'
})
except Exception as e:
db.session.rollback()
return jsonify({
'success': False,
'message': f'오류: {str(e)}'
})
@idrac_bp.route('/api/servers/<int:server_id>/firmware/compare', methods=['GET'])
def compare_server_firmware(server_id):
"""
서버 펌웨어 버전 비교
현재 버전과 최신 버전을 비교하여 업데이트 필요 여부 확인
"""
try:
server = IdracServer.query.get(server_id)
if not server:
return jsonify({
'success': False,
'message': '서버를 찾을 수 없습니다'
})
# 현재 펌웨어 조회
client = DellRedfishClient(server.ip_address, server.username, server.password)
current_inventory = client.get_firmware_inventory()
# 최신 버전 정보 조회
latest_versions = FirmwareVersion.query.filter_by(is_active=True).all()
# 비교 결과
comparisons = []
for current_fw in current_inventory:
component_name = current_fw['Name']
current_version = current_fw['Version']
# 최신 버전 찾기 (컴포넌트명 매칭)
latest = None
for lv in latest_versions:
if lv.component_name.lower() in component_name.lower():
# 서버 모델 확인
if not lv.server_model or lv.server_model == server.model:
latest = lv
break
# 비교
comparison = FirmwareComparisonResult(
component_name=component_name,
current_version=current_version,
latest_version=latest.latest_version if latest else None
)
result = comparison.to_dict()
# 추가 정보
if latest:
result['latest_info'] = {
'release_date': latest.release_date,
'download_url': latest.download_url,
'file_name': latest.file_name,
'is_critical': latest.is_critical,
'notes': latest.notes
}
comparisons.append(result)
# 통계
total = len(comparisons)
outdated = len([c for c in comparisons if c['status'] == 'outdated'])
latest_count = len([c for c in comparisons if c['status'] == 'latest'])
unknown = len([c for c in comparisons if c['status'] == 'unknown'])
return jsonify({
'success': True,
'server': {
'id': server.id,
'name': server.name,
'model': server.model
},
'comparisons': comparisons,
'summary': {
'total': total,
'outdated': outdated,
'latest': latest_count,
'unknown': unknown
}
})
except Exception as e:
return jsonify({
'success': False,
'message': f'오류: {str(e)}'
})
@idrac_bp.route('/api/servers/firmware/compare-multi', methods=['POST'])
def compare_multi_servers_firmware():
"""
여러 서버의 펌웨어 버전 비교
"""
try:
data = request.json
server_ids = data.get('server_ids', [])
if not server_ids:
return jsonify({
'success': False,
'message': '서버를 선택하세요'
})
results = []
for server_id in server_ids:
server = IdracServer.query.get(server_id)
if not server:
continue
try:
# 각 서버 비교
client = DellRedfishClient(server.ip_address, server.username, server.password)
current_inventory = client.get_firmware_inventory()
latest_versions = FirmwareVersion.query.filter_by(is_active=True).all()
outdated_count = 0
outdated_items = []
for current_fw in current_inventory:
component_name = current_fw['Name']
current_version = current_fw['Version']
# 최신 버전 찾기
for lv in latest_versions:
if lv.component_name.lower() in component_name.lower():
comparison = FirmwareComparisonResult(
component_name=component_name,
current_version=current_version,
latest_version=lv.latest_version
)
if comparison.status == 'outdated':
outdated_count += 1
outdated_items.append({
'component': component_name,
'current': current_version,
'latest': lv.latest_version
})
break
results.append({
'server_id': server.id,
'server_name': server.name,
'outdated_count': outdated_count,
'outdated_items': outdated_items,
'status': 'needs_update' if outdated_count > 0 else 'up_to_date'
})
except Exception as e:
results.append({
'server_id': server.id,
'server_name': server.name,
'error': str(e)
})
return jsonify({
'success': True,
'results': results
})
except Exception as e:
return jsonify({
'success': False,
'message': f'오류: {str(e)}'
})
# ========================================
# 초기 데이터 생성 함수
# ========================================
def init_firmware_versions():
"""초기 펌웨어 버전 데이터 생성"""
initial_versions = [
{
'component_name': 'BIOS',
'component_type': 'Firmware',
'vendor': 'Dell',
'server_model': 'PowerEdge R750',
'latest_version': '2.15.0',
'release_date': '2024-01-15',
'notes': 'PowerEdge R750 최신 BIOS'
},
{
'component_name': 'iDRAC',
'component_type': 'Firmware',
'vendor': 'Dell',
'latest_version': '6.10.30.00',
'release_date': '2024-02-20',
'notes': 'iDRAC9 최신 펌웨어 (모든 모델 공용)'
},
{
'component_name': 'PERC H755',
'component_type': 'Firmware',
'vendor': 'Dell',
'server_model': 'PowerEdge R750',
'latest_version': '25.5.9.0001',
'release_date': '2024-01-10',
'notes': 'PERC H755 RAID 컨트롤러'
},
{
'component_name': 'BIOS',
'component_type': 'Firmware',
'vendor': 'Dell',
'server_model': 'PowerEdge R640',
'latest_version': '2.19.2',
'release_date': '2024-02-01',
'notes': 'PowerEdge R640 최신 BIOS'
},
{
'component_name': 'CPLD',
'component_type': 'Firmware',
'vendor': 'Dell',
'latest_version': '1.0.6',
'release_date': '2023-12-15',
'notes': '시스템 보드 CPLD (14G/15G 공용)'
},
]
for data in initial_versions:
# 중복 체크
existing = FirmwareVersion.query.filter_by(
component_name=data['component_name'],
server_model=data.get('server_model')
).first()
if not existing:
version = FirmwareVersion(**data)
db.session.add(version)
try:
db.session.commit()
print("✓ 초기 펌웨어 버전 데이터 생성 완료")
except:
db.session.rollback()
print("⚠ 초기 데이터 생성 중 오류 (이미 있을 수 있음)")
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More