This commit is contained in:
2025-11-29 11:13:55 +09:00
parent c0d3312bca
commit 19798cca66
12 changed files with 2094 additions and 255 deletions

View File

@@ -0,0 +1,353 @@
"""
Dell 펌웨어 카탈로그 대안 방법들
backend/services/dell_catalog_alternatives.py
Dell의 공식 Catalog.xml이 차단된 경우 사용할 수 있는 대안들
"""
import requests
from typing import List, Dict, Optional
from backend.models.firmware_version import FirmwareVersion, db
from datetime import datetime
class DellFirmwareCatalogAlternatives:
"""Dell 펌웨어 정보를 가져오는 대안 방법들"""
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
})
# ========================================
# 방법 1: Dell Support API (공식 API)
# ========================================
def fetch_from_dell_support_api(self, model: str = "PowerEdge R750") -> List[Dict]:
"""
Dell Support API를 통한 펌웨어 정보 조회
Dell의 공식 Support API 엔드포인트:
https://www.dell.com/support/home/api/
참고: API 키가 필요할 수 있음
"""
try:
# Dell Support API 엔드포인트 (예시)
# 실제 API는 Dell 개발자 포털에서 확인 필요
url = f"https://www.dell.com/support/home/api/products/{model}/drivers"
response = self.session.get(url, timeout=30)
if response.status_code == 200:
data = response.json()
return self._parse_support_api_response(data, model)
else:
print(f"Dell Support API 오류: {response.status_code}")
return []
except Exception as e:
print(f"Dell Support API 조회 실패: {str(e)}")
return []
# ========================================
# 방법 2: Dell TechDirect (파트너 전용)
# ========================================
def fetch_from_techdirect(self, model: str = "PowerEdge R750") -> List[Dict]:
"""
Dell TechDirect API를 통한 펌웨어 정보 조회
TechDirect는 Dell 파트너용 플랫폼
API 키 필요: https://techdirect.dell.com/
"""
# TechDirect API 구현
# 실제 사용 시 API 키 필요
pass
# ========================================
# 방법 3: 로컬 카탈로그 파일 사용
# ========================================
def load_from_local_catalog(self, catalog_path: str) -> List[Dict]:
"""
로컬에 저장된 Catalog.xml 파일 사용
사용법:
1. Dell 공식 사이트에서 수동으로 Catalog.xml 다운로드
2. 로컬에 저장
3. 이 함수로 파싱
Args:
catalog_path: 로컬 Catalog.xml 파일 경로
"""
try:
from xml.etree import ElementTree as ET
with open(catalog_path, 'r', encoding='utf-8') as f:
content = f.read()
root = ET.fromstring(content)
return self._parse_catalog_xml(root)
except Exception as e:
print(f"로컬 카탈로그 파일 로드 실패: {str(e)}")
return []
# ========================================
# 방법 4: iDRAC에서 직접 조회
# ========================================
def fetch_from_idrac(self, idrac_ip: str, username: str, password: str) -> List[Dict]:
"""
iDRAC Redfish API를 통해 사용 가능한 업데이트 조회
iDRAC는 Dell Update Service를 통해 사용 가능한 업데이트를 확인할 수 있음
장점:
- 실제 서버에 맞는 정확한 펌웨어 정보
- 외부 카탈로그 불필요
단점:
- 각 서버마다 조회 필요
- 네트워크 연결 필요
"""
try:
from backend.services.idrac_redfish_client import DellRedfishClient
client = DellRedfishClient(idrac_ip, username, password)
# UpdateService에서 사용 가능한 업데이트 조회
url = f"https://{idrac_ip}/redfish/v1/UpdateService"
response = client.session.get(url, timeout=30)
if response.status_code == 200:
data = response.json()
# UpdateService의 Actions 확인
# SimpleUpdate 또는 CheckForUpdate 액션 사용
return self._parse_idrac_updates(data)
return []
except Exception as e:
print(f"iDRAC 업데이트 조회 실패: {str(e)}")
return []
# ========================================
# 방법 5: 수동 입력 (가장 간단하고 확실)
# ========================================
def create_manual_catalog(self) -> List[Dict]:
"""
수동으로 작성한 펌웨어 버전 정보
Dell 공식 사이트에서 확인한 최신 버전을 수동으로 입력
https://www.dell.com/support/
장점:
- 가장 확실하고 안정적
- 외부 의존성 없음
- 검증된 버전만 사용
단점:
- 수동 업데이트 필요
"""
manual_catalog = [
{
'component_name': 'BIOS',
'latest_version': '2.15.2',
'server_model': 'PowerEdge R750',
'vendor': 'Dell',
'release_date': '2024-03-15',
'download_url': 'https://www.dell.com/support/home/drivers/driversdetails?driverid=...',
'notes': 'PowerEdge R750 BIOS - 2024년 3월 릴리즈',
'is_critical': False
},
{
'component_name': 'iDRAC',
'latest_version': '7.00.00.00',
'server_model': None, # 모든 모델
'vendor': 'Dell',
'release_date': '2024-02-20',
'download_url': 'https://www.dell.com/support/home/drivers/driversdetails?driverid=...',
'notes': 'iDRAC9 최신 펌웨어 (14G/15G/16G 공용)',
'is_critical': True
},
{
'component_name': 'PERC H755',
'latest_version': '25.5.9.0001',
'server_model': 'PowerEdge R750',
'vendor': 'Dell',
'release_date': '2024-01-10',
'notes': 'PERC H755 RAID 컨트롤러',
'is_critical': False
},
# 더 많은 컴포넌트 추가...
]
return manual_catalog
# ========================================
# 방법 6: Dell Repository Manager (DRM)
# ========================================
def fetch_from_drm_export(self, drm_export_path: str) -> List[Dict]:
"""
Dell Repository Manager (DRM)에서 내보낸 데이터 사용
DRM은 Dell의 공식 펌웨어 관리 도구
https://www.dell.com/support/kbdoc/en-us/000177083/
사용법:
1. DRM 설치 및 실행
2. 필요한 서버 모델 선택
3. 카탈로그 내보내기
4. 내보낸 파일을 이 함수로 파싱
"""
try:
# DRM 내보내기 파일 파싱 로직
# XML 또는 CSV 형식일 수 있음
pass
except Exception as e:
print(f"DRM 내보내기 파일 로드 실패: {str(e)}")
return []
# ========================================
# 헬퍼 함수들
# ========================================
def _parse_support_api_response(self, data: Dict, model: str) -> List[Dict]:
"""Dell Support API 응답 파싱"""
# API 응답 구조에 따라 구현
return []
def _parse_catalog_xml(self, root) -> List[Dict]:
"""Catalog.xml 파싱"""
from xml.etree import ElementTree as ET
firmware_list = []
for pkg in root.findall(".//SoftwareComponent"):
name = pkg.findtext("Name")
version = pkg.findtext("Version")
release_date = pkg.findtext("ReleaseDate")
path = pkg.findtext("path")
if name and version:
firmware_list.append({
'component_name': name,
'latest_version': version,
'release_date': release_date,
'download_url': f"https://downloads.dell.com/{path}" if path else None,
'vendor': 'Dell'
})
return firmware_list
def _parse_idrac_updates(self, data: Dict) -> List[Dict]:
"""iDRAC UpdateService 응답 파싱"""
# UpdateService 데이터 파싱
return []
# ========================================
# DB에 저장
# ========================================
def save_to_database(self, firmware_list: List[Dict]) -> int:
"""
펌웨어 목록을 데이터베이스에 저장
Returns:
저장된 항목 수
"""
count = 0
for fw in firmware_list:
# 중복 확인
existing = FirmwareVersion.query.filter_by(
component_name=fw.get('component_name'),
latest_version=fw.get('latest_version'),
server_model=fw.get('server_model')
).first()
if not existing:
version = FirmwareVersion(
component_name=fw.get('component_name'),
latest_version=fw.get('latest_version'),
server_model=fw.get('server_model'),
vendor=fw.get('vendor', 'Dell'),
release_date=fw.get('release_date'),
download_url=fw.get('download_url'),
notes=fw.get('notes'),
is_critical=fw.get('is_critical', False)
)
db.session.add(version)
count += 1
db.session.commit()
return count
# ========================================
# 사용 예시
# ========================================
def sync_firmware_alternative(method: str = 'manual', **kwargs) -> Dict:
"""
대안 방법으로 펌웨어 정보 동기화
Args:
method: 'manual', 'local_catalog', 'idrac', 'support_api' 중 선택
**kwargs: 각 방법에 필요한 추가 인자
Returns:
{'success': bool, 'count': int, 'message': str}
"""
catalog = DellFirmwareCatalogAlternatives()
firmware_list = []
try:
if method == 'manual':
# 가장 권장: 수동으로 작성한 카탈로그
firmware_list = catalog.create_manual_catalog()
elif method == 'local_catalog':
# 로컬 Catalog.xml 파일 사용
catalog_path = kwargs.get('catalog_path')
if not catalog_path:
return {'success': False, 'count': 0, 'message': 'catalog_path 필요'}
firmware_list = catalog.load_from_local_catalog(catalog_path)
elif method == 'idrac':
# iDRAC에서 직접 조회
idrac_ip = kwargs.get('idrac_ip')
username = kwargs.get('username')
password = kwargs.get('password')
if not all([idrac_ip, username, password]):
return {'success': False, 'count': 0, 'message': 'iDRAC 정보 필요'}
firmware_list = catalog.fetch_from_idrac(idrac_ip, username, password)
elif method == 'support_api':
# Dell Support API 사용
model = kwargs.get('model', 'PowerEdge R750')
firmware_list = catalog.fetch_from_dell_support_api(model)
else:
return {'success': False, 'count': 0, 'message': f'알 수 없는 방법: {method}'}
# DB에 저장
count = catalog.save_to_database(firmware_list)
return {
'success': True,
'count': count,
'message': f'{count}개 펌웨어 정보 동기화 완료'
}
except Exception as e:
return {
'success': False,
'count': 0,
'message': f'오류: {str(e)}'
}

View File

@@ -1,58 +1,178 @@
import requests
from xml.etree import ElementTree as ET
from backend.models.firmware_version import FirmwareVersion, db
from datetime import datetime
def get_manual_firmware_catalog(model="PowerEdge R750"):
"""
수동으로 작성한 펌웨어 카탈로그
Dell 공식 사이트에서 확인한 최신 버전 정보
https://www.dell.com/support/
Dell Catalog.xml이 차단된 경우 이 데이터 사용
"""
# 2024년 11월 기준 최신 버전 (정기적으로 업데이트 필요)
catalog = {
"PowerEdge R750": [
{
'component_name': 'BIOS',
'latest_version': '2.15.2',
'release_date': '2024-03-15',
'notes': 'PowerEdge R750 BIOS - 보안 업데이트 포함',
'is_critical': False
},
{
'component_name': 'iDRAC',
'latest_version': '7.00.00.00',
'release_date': '2024-02-20',
'notes': 'iDRAC9 최신 펌웨어',
'is_critical': True
},
{
'component_name': 'PERC H755',
'latest_version': '25.5.9.0001',
'release_date': '2024-01-10',
'notes': 'PERC H755 RAID 컨트롤러',
'is_critical': False
},
{
'component_name': 'CPLD',
'latest_version': '1.0.6',
'release_date': '2023-12-15',
'notes': '시스템 보드 CPLD',
'is_critical': False
},
],
"PowerEdge R640": [
{
'component_name': 'BIOS',
'latest_version': '2.19.2',
'release_date': '2024-02-01',
'notes': 'PowerEdge R640 BIOS',
'is_critical': False
},
{
'component_name': 'iDRAC',
'latest_version': '7.00.00.00',
'release_date': '2024-02-20',
'notes': 'iDRAC9 최신 펌웨어',
'is_critical': True
},
]
}
return catalog.get(model, [])
def sync_dell_catalog(model="PowerEdge R750"):
"""
Dell 공식 Catalog.xml에서 지정된 모델(model)에 해당하는 펌웨어 정보만 추출 후 DB 저장.
Dell 펌웨어 정보 동기화
1차: Dell 공식 Catalog.xml 시도
2차: 수동 카탈로그 사용 (Fallback)
"""
url = "https://downloads.dell.com/catalog/Catalog.xml"
print(f"[INFO] Dell Catalog 다운로드 중... ({url})")
response = requests.get(url, timeout=60)
response.raise_for_status()
root = ET.fromstring(response.content)
count = 0
# 1차 시도: Dell 공식 Catalog.xml
try:
url = "https://downloads.dell.com/catalog/Catalog.xml"
print(f"[INFO] Dell Catalog 다운로드 시도... ({url})")
response = requests.get(url, timeout=30)
response.raise_for_status()
root = ET.fromstring(response.content)
for pkg in root.findall(".//SoftwareComponent"):
# 이 컴포넌트가 지정된 모델에 해당하는지 확인
supported = [
sys.text.strip()
for sys in pkg.findall(".//SupportedSystems/Brand/Model/Display")
if sys.text
]
if model not in supported:
continue
for pkg in root.findall(".//SoftwareComponent"):
# 이 컴포넌트가 지정된 모델에 해당하는지 확인
supported = [
sys.text.strip()
for sys in pkg.findall(".//SupportedSystems/Brand/Model/Display")
if sys.text
]
if model not in supported:
continue
name = pkg.findtext("Name")
version = pkg.findtext("Version")
release_date = pkg.findtext("ReleaseDate")
path = pkg.findtext("path")
vendor = "Dell"
name = pkg.findtext("Name")
version = pkg.findtext("Version")
release_date = pkg.findtext("ReleaseDate")
path = pkg.findtext("path")
vendor = "Dell"
if not name or not version:
continue
if not name or not version:
continue
# 중복 방지
existing = FirmwareVersion.query.filter_by(
component_name=name,
latest_version=version,
server_model=model
).first()
# 중복 방지
existing = FirmwareVersion.query.filter_by(
component_name=name,
latest_version=version,
server_model=model
).first()
if not existing:
db.session.add(
FirmwareVersion(
component_name=name,
latest_version=version,
release_date=release_date,
vendor=vendor,
server_model=model,
download_url=f"https://downloads.dell.com/{path}",
if not existing:
db.session.add(
FirmwareVersion(
component_name=name,
latest_version=version,
release_date=release_date,
vendor=vendor,
server_model=model,
download_url=f"https://downloads.dell.com/{path}",
)
)
)
count += 1
count += 1
db.session.commit()
print(f"{model} 관련 펌웨어 {count}개 동기화 완료 (Dell Catalog)")
return count
except requests.exceptions.HTTPError as e:
if e.response.status_code == 404:
print(f"[경고] Dell Catalog.xml 접근 불가 (404). 수동 카탈로그 사용...")
else:
print(f"[경고] Dell Catalog 다운로드 실패: {e}. 수동 카탈로그 사용...")
except Exception as e:
print(f"[경고] Dell Catalog 처리 중 오류: {e}. 수동 카탈로그 사용...")
# 2차 시도: 수동 카탈로그 사용
try:
print(f"[INFO] 수동 카탈로그에서 {model} 펌웨어 정보 로드 중...")
manual_catalog = get_manual_firmware_catalog(model)
if not manual_catalog:
print(f"[경고] {model}에 대한 수동 카탈로그 데이터가 없습니다")
return 0
for fw in manual_catalog:
# 중복 방지
existing = FirmwareVersion.query.filter_by(
component_name=fw['component_name'],
latest_version=fw['latest_version'],
server_model=model
).first()
if not existing:
db.session.add(
FirmwareVersion(
component_name=fw['component_name'],
latest_version=fw['latest_version'],
release_date=fw.get('release_date'),
vendor='Dell',
server_model=model,
notes=fw.get('notes'),
is_critical=fw.get('is_critical', False)
)
)
count += 1
db.session.commit()
print(f"{model} 관련 펌웨어 {count}개 동기화 완료 (수동 카탈로그)")
return count
except Exception as e:
print(f"[오류] 수동 카탈로그 처리 실패: {e}")
db.session.rollback()
return 0
db.session.commit()
print(f"{model} 관련 펌웨어 {count}개 동기화 완료")
return count

View File

@@ -0,0 +1,308 @@
"""
Dell Repository Manager (DRM) 연동 모듈
backend/services/drm_catalog_sync.py
Dell Repository Manager에서 생성한 로컬 리포지토리 카탈로그를 파싱하여
펌웨어 정보를 가져오는 기능
"""
import os
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import List, Dict, Optional
from backend.models.firmware_version import FirmwareVersion, db
from datetime import datetime
class DRMCatalogSync:
"""Dell Repository Manager 카탈로그 동기화"""
def __init__(self, drm_repository_path: str = None):
"""
Args:
drm_repository_path: DRM 리포지토리 경로
예: C:/Dell/Repository 또는 네트워크 경로
"""
self.repository_path = drm_repository_path
self.catalog_file = None
if drm_repository_path:
# DRM은 보통 catalog 폴더에 XML 파일 생성
possible_catalogs = [
os.path.join(drm_repository_path, 'catalog', 'Catalog.xml'),
os.path.join(drm_repository_path, 'Catalog.xml'),
os.path.join(drm_repository_path, 'catalog.xml'),
]
for catalog_path in possible_catalogs:
if os.path.exists(catalog_path):
self.catalog_file = catalog_path
print(f"[INFO] DRM 카탈로그 파일 발견: {catalog_path}")
break
def sync_from_drm_repository(self, model: str = "PowerEdge R750") -> int:
"""
DRM 리포지토리에서 펌웨어 정보 동기화
Args:
model: 서버 모델명
Returns:
동기화된 펌웨어 개수
"""
if not self.catalog_file or not os.path.exists(self.catalog_file):
print(f"[오류] DRM 카탈로그 파일을 찾을 수 없습니다: {self.repository_path}")
return 0
try:
print(f"[INFO] DRM 카탈로그 파싱 중: {self.catalog_file}")
# XML 파싱
tree = ET.parse(self.catalog_file)
root = tree.getroot()
count = 0
# DRM 카탈로그 구조 파싱
for pkg in root.findall(".//SoftwareComponent"):
# 모델 필터링
supported_models = self._get_supported_models(pkg)
if model and model not in supported_models:
continue
# 펌웨어 정보 추출
firmware_info = self._extract_firmware_info(pkg, model)
if firmware_info:
# DB에 저장
if self._save_firmware_to_db(firmware_info):
count += 1
print(f"✓ DRM에서 {model} 관련 펌웨어 {count}개 동기화 완료")
return count
except Exception as e:
print(f"[오류] DRM 카탈로그 동기화 실패: {str(e)}")
import traceback
traceback.print_exc()
return 0
def _get_supported_models(self, pkg_element) -> List[str]:
"""패키지가 지원하는 서버 모델 목록 추출"""
models = []
# Dell 카탈로그 XML 구조
for display in pkg_element.findall(".//SupportedSystems/Brand/Model/Display"):
if display.text:
models.append(display.text.strip())
return models
def _extract_firmware_info(self, pkg_element, model: str) -> Optional[Dict]:
"""패키지에서 펌웨어 정보 추출"""
try:
name = pkg_element.findtext("Name")
version = pkg_element.findtext("vendorVersion") # DRM은 vendorVersion 사용
if not version:
version = pkg_element.findtext("dellVersion") # 또는 dellVersion
release_date = pkg_element.findtext("dateTime")
path = pkg_element.findtext("path")
category = pkg_element.findtext("Category")
# 중요도 판단
importance = pkg_element.findtext("ImportanceDisplay")
is_critical = importance and "Critical" in importance
# 파일 정보
file_name = None
file_size_mb = None
if path:
file_name = os.path.basename(path)
# 실제 파일이 있으면 크기 확인
if self.repository_path:
full_path = os.path.join(self.repository_path, path)
if os.path.exists(full_path):
file_size_mb = os.path.getsize(full_path) // (1024 * 1024)
if not name or not version:
return None
return {
'component_name': name,
'latest_version': version,
'server_model': model,
'vendor': 'Dell',
'release_date': self._parse_date(release_date),
'download_url': f"https://downloads.dell.com/{path}" if path else None,
'file_name': file_name,
'file_size_mb': file_size_mb,
'component_type': category,
'is_critical': is_critical,
'notes': f"DRM에서 동기화 - {category}" if category else "DRM에서 동기화"
}
except Exception as e:
print(f"[경고] 펌웨어 정보 추출 실패: {str(e)}")
return None
def _parse_date(self, date_str: str) -> Optional[str]:
"""날짜 문자열 파싱"""
if not date_str:
return None
try:
# DRM 날짜 형식: 2024-03-15T10:30:00
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
return dt.strftime('%Y-%m-%d')
except:
return date_str[:10] if len(date_str) >= 10 else None
def _save_firmware_to_db(self, firmware_info: Dict) -> bool:
"""펌웨어 정보를 DB에 저장"""
try:
# 중복 확인
existing = FirmwareVersion.query.filter_by(
component_name=firmware_info['component_name'],
latest_version=firmware_info['latest_version'],
server_model=firmware_info['server_model']
).first()
if existing:
# 이미 존재하면 업데이트
for key, value in firmware_info.items():
if hasattr(existing, key) and value is not None:
setattr(existing, key, value)
db.session.commit()
return False # 새로 추가된 것은 아님
else:
# 새로 추가
version = FirmwareVersion(**firmware_info)
db.session.add(version)
db.session.commit()
return True
except Exception as e:
print(f"[오류] DB 저장 실패: {str(e)}")
db.session.rollback()
return False
def list_available_models(self) -> List[str]:
"""DRM 리포지토리에서 사용 가능한 모델 목록 조회"""
if not self.catalog_file or not os.path.exists(self.catalog_file):
return []
try:
tree = ET.parse(self.catalog_file)
root = tree.getroot()
models = set()
for display in root.findall(".//SupportedSystems/Brand/Model/Display"):
if display.text:
models.add(display.text.strip())
return sorted(list(models))
except Exception as e:
print(f"[오류] 모델 목록 조회 실패: {str(e)}")
return []
def get_repository_info(self) -> Dict:
"""DRM 리포지토리 정보 조회"""
if not self.catalog_file or not os.path.exists(self.catalog_file):
return {
'exists': False,
'path': self.repository_path,
'catalog_file': None
}
try:
tree = ET.parse(self.catalog_file)
root = tree.getroot()
# 카탈로그 메타데이터
base_location = root.findtext(".//BaseLocation")
release_id = root.findtext(".//ReleaseID")
# 패키지 수 계산
total_packages = len(root.findall(".//SoftwareComponent"))
return {
'exists': True,
'path': self.repository_path,
'catalog_file': self.catalog_file,
'base_location': base_location,
'release_id': release_id,
'total_packages': total_packages,
'available_models': self.list_available_models()
}
except Exception as e:
return {
'exists': True,
'path': self.repository_path,
'catalog_file': self.catalog_file,
'error': str(e)
}
# ========================================
# 편의 함수
# ========================================
def sync_from_drm(repository_path: str, model: str = "PowerEdge R750") -> Dict:
"""
DRM 리포지토리에서 펌웨어 동기화 (간편 함수)
Args:
repository_path: DRM 리포지토리 경로
model: 서버 모델명
Returns:
{'success': bool, 'count': int, 'message': str}
"""
try:
drm = DRMCatalogSync(repository_path)
# 리포지토리 확인
info = drm.get_repository_info()
if not info['exists']:
return {
'success': False,
'count': 0,
'message': f'DRM 리포지토리를 찾을 수 없습니다: {repository_path}'
}
# 동기화
count = drm.sync_from_drm_repository(model)
return {
'success': True,
'count': count,
'message': f'{model} 펌웨어 {count}개 동기화 완료',
'repository_info': info
}
except Exception as e:
return {
'success': False,
'count': 0,
'message': f'DRM 동기화 실패: {str(e)}'
}
def check_drm_repository(repository_path: str) -> Dict:
"""
DRM 리포지토리 상태 확인
Args:
repository_path: DRM 리포지토리 경로
Returns:
리포지토리 정보
"""
drm = DRMCatalogSync(repository_path)
return drm.get_repository_info()