update
This commit is contained in:
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.
@@ -0,0 +1,58 @@
|
||||
import requests
|
||||
from xml.etree import ElementTree as ET
|
||||
from backend.models.firmware_version import FirmwareVersion, db
|
||||
|
||||
def sync_dell_catalog(model="PowerEdge R750"):
|
||||
"""
|
||||
Dell 공식 Catalog.xml에서 지정된 모델(model)에 해당하는 펌웨어 정보만 추출 후 DB 저장.
|
||||
"""
|
||||
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
|
||||
|
||||
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"
|
||||
|
||||
if not name or not version:
|
||||
continue
|
||||
|
||||
# 중복 방지
|
||||
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}",
|
||||
)
|
||||
)
|
||||
count += 1
|
||||
|
||||
db.session.commit()
|
||||
print(f"✓ {model} 관련 펌웨어 {count}개 동기화 완료")
|
||||
return count
|
||||
@@ -0,0 +1,397 @@
|
||||
"""
|
||||
Dell iDRAC REDFISH API 클라이언트
|
||||
Dell 서버 펌웨어 조회 최적화 버전
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
# SSL 경고 비활성화
|
||||
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
|
||||
|
||||
|
||||
class DellRedfishClient:
|
||||
def __init__(self, idrac_ip, username, password):
|
||||
"""
|
||||
Dell iDRAC REDFISH 클라이언트 초기화
|
||||
|
||||
Args:
|
||||
idrac_ip: iDRAC IP 주소
|
||||
username: iDRAC 사용자명
|
||||
password: iDRAC 비밀번호
|
||||
"""
|
||||
self.idrac_ip = idrac_ip
|
||||
self.base_url = f"https://{idrac_ip}"
|
||||
self.auth = HTTPBasicAuth(username, password)
|
||||
self.headers = {'Content-Type': 'application/json'}
|
||||
self.session = requests.Session()
|
||||
self.session.verify = False
|
||||
self.session.auth = self.auth
|
||||
|
||||
def check_connection(self):
|
||||
"""iDRAC 연결 확인"""
|
||||
try:
|
||||
url = f"{self.base_url}/redfish/v1"
|
||||
response = self.session.get(url, timeout=10)
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
print(f"연결 오류: {str(e)}")
|
||||
return False
|
||||
|
||||
def get_firmware_inventory(self):
|
||||
"""
|
||||
현재 설치된 펌웨어 목록 조회 (Dell 최적화)
|
||||
주요 컴포넌트만 필터링하여 반환
|
||||
"""
|
||||
try:
|
||||
url = f"{self.base_url}/redfish/v1/UpdateService/FirmwareInventory"
|
||||
response = self.session.get(url, timeout=30)
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"펌웨어 목록 조회 실패: {response.status_code}")
|
||||
return []
|
||||
|
||||
data = response.json()
|
||||
inventory = []
|
||||
|
||||
# 주요 컴포넌트 타입 (Dell 서버)
|
||||
important_components = [
|
||||
'BIOS', 'iDRAC', 'CPLD', 'Diagnostics',
|
||||
'Driver', 'Firmware', 'USC', 'NIC',
|
||||
'RAID', 'PERC', 'Storage', 'Backplane',
|
||||
'HBA', 'Network', 'Intel', 'Broadcom',
|
||||
'Mellanox', 'Emulex', 'QLogic'
|
||||
]
|
||||
|
||||
print(f"전체 펌웨어 항목 수: {len(data.get('Members', []))}")
|
||||
|
||||
# 각 펌웨어 항목 조회
|
||||
for idx, member in enumerate(data.get('Members', [])):
|
||||
try:
|
||||
fw_url = f"{self.base_url}{member.get('@odata.id', '')}"
|
||||
fw_response = self.session.get(fw_url, timeout=10)
|
||||
|
||||
if fw_response.status_code == 200:
|
||||
fw_data = fw_response.json()
|
||||
|
||||
# 이름과 버전 추출 (여러 필드 시도)
|
||||
name = (fw_data.get('Name') or
|
||||
fw_data.get('SoftwareId') or
|
||||
fw_data.get('Id') or
|
||||
'Unknown')
|
||||
|
||||
version = (fw_data.get('Version') or
|
||||
fw_data.get('VersionString') or
|
||||
'Unknown')
|
||||
|
||||
# 컴포넌트 타입 확인
|
||||
component_type = fw_data.get('ComponentType', '')
|
||||
|
||||
# 중요 컴포넌트만 필터링
|
||||
is_important = any(comp.lower() in name.lower()
|
||||
for comp in important_components)
|
||||
|
||||
if is_important or component_type:
|
||||
item = {
|
||||
'Name': name,
|
||||
'Version': version,
|
||||
'ComponentType': component_type,
|
||||
'Updateable': fw_data.get('Updateable', False),
|
||||
'Status': fw_data.get('Status', {}).get('Health', 'OK'),
|
||||
'Id': fw_data.get('Id', ''),
|
||||
'Description': fw_data.get('Description', ''),
|
||||
'ReleaseDate': fw_data.get('ReleaseDate', '')
|
||||
}
|
||||
|
||||
inventory.append(item)
|
||||
|
||||
# 진행 상황 출력
|
||||
if (idx + 1) % 10 == 0:
|
||||
print(f"조회 중... {idx + 1}/{len(data.get('Members', []))}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"펌웨어 항목 조회 오류 ({idx}): {str(e)}")
|
||||
continue
|
||||
|
||||
# 이름순 정렬
|
||||
inventory.sort(key=lambda x: x['Name'])
|
||||
|
||||
print(f"중요 펌웨어 항목 수: {len(inventory)}")
|
||||
|
||||
# 주요 컴포넌트 요약
|
||||
bios = next((x for x in inventory if 'BIOS' in x['Name']), None)
|
||||
idrac = next((x for x in inventory if 'iDRAC' in x['Name']), None)
|
||||
|
||||
if bios:
|
||||
print(f"✓ BIOS 버전: {bios['Version']}")
|
||||
if idrac:
|
||||
print(f"✓ iDRAC 버전: {idrac['Version']}")
|
||||
|
||||
return inventory
|
||||
|
||||
except Exception as e:
|
||||
print(f"펌웨어 목록 조회 오류: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
def get_firmware_summary(self):
|
||||
"""
|
||||
주요 펌웨어 버전만 간단히 조회
|
||||
BIOS, iDRAC, PERC 등 핵심 컴포넌트만
|
||||
"""
|
||||
try:
|
||||
inventory = self.get_firmware_inventory()
|
||||
|
||||
summary = {
|
||||
'BIOS': 'N/A',
|
||||
'iDRAC': 'N/A',
|
||||
'PERC': 'N/A',
|
||||
'NIC': 'N/A',
|
||||
'CPLD': 'N/A'
|
||||
}
|
||||
|
||||
for item in inventory:
|
||||
name = item['Name']
|
||||
version = item['Version']
|
||||
|
||||
if 'BIOS' in name:
|
||||
summary['BIOS'] = version
|
||||
elif 'iDRAC' in name or 'Integrated Dell Remote Access' in name:
|
||||
summary['iDRAC'] = version
|
||||
elif 'PERC' in name or 'RAID' in name:
|
||||
if summary['PERC'] == 'N/A':
|
||||
summary['PERC'] = version
|
||||
elif 'NIC' in name or 'Network' in name:
|
||||
if summary['NIC'] == 'N/A':
|
||||
summary['NIC'] = version
|
||||
elif 'CPLD' in name:
|
||||
summary['CPLD'] = version
|
||||
|
||||
return summary
|
||||
|
||||
except Exception as e:
|
||||
print(f"펌웨어 요약 조회 오류: {str(e)}")
|
||||
return {}
|
||||
|
||||
def upload_firmware_staged(self, dup_file_path):
|
||||
"""
|
||||
DUP 파일을 iDRAC에 업로드 (스테이징만, 재부팅 시 적용)
|
||||
|
||||
Args:
|
||||
dup_file_path: 업로드할 DUP 파일 경로
|
||||
|
||||
Returns:
|
||||
dict: {'success': bool, 'job_id': str, 'message': str}
|
||||
"""
|
||||
import os
|
||||
if not os.path.exists(dup_file_path):
|
||||
return {
|
||||
'success': False,
|
||||
'job_id': None,
|
||||
'message': f'파일을 찾을 수 없습니다: {dup_file_path}'
|
||||
}
|
||||
|
||||
file_name = os.path.basename(dup_file_path)
|
||||
file_size = os.path.getsize(dup_file_path)
|
||||
|
||||
print(f"파일 업로드: {file_name} ({file_size / (1024*1024):.2f} MB)")
|
||||
|
||||
try:
|
||||
url = f"{self.base_url}/redfish/v1/UpdateService/MultipartUpload"
|
||||
|
||||
# Multipart 업로드 준비
|
||||
with open(dup_file_path, 'rb') as f:
|
||||
files = {
|
||||
'file': (file_name, f, 'application/octet-stream')
|
||||
}
|
||||
|
||||
# targets=INSTALLED 파라미터로 스테이징 모드 설정
|
||||
data = {
|
||||
'@Redfish.OperationApplyTime': 'OnReset',
|
||||
'Targets': []
|
||||
}
|
||||
|
||||
multipart_data = {
|
||||
'UpdateParameters': (None, json.dumps(data), 'application/json')
|
||||
}
|
||||
multipart_data.update(files)
|
||||
|
||||
print("업로드 시작...")
|
||||
response = self.session.post(
|
||||
url,
|
||||
files=multipart_data,
|
||||
auth=self.auth,
|
||||
verify=False,
|
||||
timeout=600 # 10분 타임아웃
|
||||
)
|
||||
|
||||
print(f"응답 코드: {response.status_code}")
|
||||
|
||||
if response.status_code in [200, 201, 202]:
|
||||
response_data = response.json()
|
||||
|
||||
# Task 또는 Job ID 추출
|
||||
task_id = None
|
||||
if '@odata.id' in response_data:
|
||||
task_id = response_data['@odata.id'].split('/')[-1]
|
||||
elif 'Id' in response_data:
|
||||
task_id = response_data['Id']
|
||||
|
||||
# Location 헤더에서 Job ID 추출
|
||||
location = response.headers.get('Location', '')
|
||||
if location and not task_id:
|
||||
task_id = location.split('/')[-1]
|
||||
|
||||
print(f"업로드 완료! Task/Job ID: {task_id}")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'job_id': task_id or 'STAGED',
|
||||
'message': '업로드 완료 (재부팅 시 적용)'
|
||||
}
|
||||
else:
|
||||
error_msg = response.text
|
||||
print(f"업로드 실패: {error_msg}")
|
||||
return {
|
||||
'success': False,
|
||||
'job_id': None,
|
||||
'message': f'업로드 실패 (코드: {response.status_code})'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"업로드 오류: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {
|
||||
'success': False,
|
||||
'job_id': None,
|
||||
'message': f'업로드 오류: {str(e)}'
|
||||
}
|
||||
|
||||
def get_job_queue(self):
|
||||
"""Job Queue 조회"""
|
||||
try:
|
||||
url = f"{self.base_url}/redfish/v1/Managers/iDRAC.Embedded.1/Jobs"
|
||||
response = self.session.get(url, timeout=30)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
jobs = []
|
||||
|
||||
for member in data.get('Members', [])[:20]: # 최근 20개
|
||||
job_url = f"{self.base_url}{member['@odata.id']}"
|
||||
job_response = self.session.get(job_url, timeout=10)
|
||||
|
||||
if job_response.status_code == 200:
|
||||
job_data = job_response.json()
|
||||
jobs.append({
|
||||
'Id': job_data.get('Id', ''),
|
||||
'Name': job_data.get('Name', ''),
|
||||
'JobState': job_data.get('JobState', 'Unknown'),
|
||||
'PercentComplete': job_data.get('PercentComplete', 0),
|
||||
'Message': job_data.get('Message', ''),
|
||||
'StartTime': job_data.get('StartTime', ''),
|
||||
'EndTime': job_data.get('EndTime', '')
|
||||
})
|
||||
|
||||
return jobs
|
||||
else:
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
print(f"Job Queue 조회 오류: {str(e)}")
|
||||
return []
|
||||
|
||||
def get_job_status(self, job_id):
|
||||
"""특정 Job 상태 조회"""
|
||||
try:
|
||||
url = f"{self.base_url}/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/{job_id}"
|
||||
response = self.session.get(url, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return {
|
||||
'Id': data.get('Id', ''),
|
||||
'Name': data.get('Name', ''),
|
||||
'JobState': data.get('JobState', 'Unknown'),
|
||||
'PercentComplete': data.get('PercentComplete', 0),
|
||||
'Message': data.get('Message', ''),
|
||||
'StartTime': data.get('StartTime', ''),
|
||||
'EndTime': data.get('EndTime', '')
|
||||
}
|
||||
else:
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"Job 상태 조회 오류: {str(e)}")
|
||||
return None
|
||||
|
||||
def delete_job(self, job_id):
|
||||
"""Job 삭제"""
|
||||
try:
|
||||
url = f"{self.base_url}/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/{job_id}"
|
||||
response = self.session.delete(url, timeout=10)
|
||||
return response.status_code in [200, 204]
|
||||
except Exception as e:
|
||||
print(f"Job 삭제 오류: {str(e)}")
|
||||
return False
|
||||
|
||||
def get_power_status(self):
|
||||
"""서버 전원 상태 조회"""
|
||||
try:
|
||||
url = f"{self.base_url}/redfish/v1/Systems/System.Embedded.1"
|
||||
response = self.session.get(url, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return {
|
||||
'PowerState': data.get('PowerState', 'Unknown'),
|
||||
'Status': data.get('Status', {}).get('Health', 'Unknown')
|
||||
}
|
||||
else:
|
||||
return {'PowerState': 'Unknown', 'Status': 'Unknown'}
|
||||
|
||||
except Exception as e:
|
||||
print(f"전원 상태 조회 오류: {str(e)}")
|
||||
return {'PowerState': 'Unknown', 'Status': 'Unknown'}
|
||||
|
||||
def reboot_server(self, reset_type='GracefulRestart'):
|
||||
"""
|
||||
서버 재부팅
|
||||
|
||||
Args:
|
||||
reset_type: GracefulRestart, ForceRestart, GracefulShutdown, ForceOff, On
|
||||
"""
|
||||
try:
|
||||
url = f"{self.base_url}/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset"
|
||||
payload = {'ResetType': reset_type}
|
||||
|
||||
response = self.session.post(
|
||||
url,
|
||||
data=json.dumps(payload),
|
||||
headers=self.headers,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
return response.status_code in [200, 202, 204]
|
||||
|
||||
except Exception as e:
|
||||
print(f"재부팅 오류: {str(e)}")
|
||||
return False
|
||||
|
||||
def power_on_server(self):
|
||||
"""서버 전원 켜기"""
|
||||
return self.reboot_server('On')
|
||||
|
||||
def power_off_server(self, shutdown_type='GracefulShutdown'):
|
||||
"""
|
||||
서버 전원 끄기
|
||||
|
||||
Args:
|
||||
shutdown_type: GracefulShutdown 또는 ForceOff
|
||||
"""
|
||||
return self.reboot_server(shutdown_type)
|
||||
Reference in New Issue
Block a user