398 lines
15 KiB
Python
398 lines
15 KiB
Python
"""
|
|
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)
|