Files
iDRAC_Info/backend/services/idrac_redfish_client.py
T
unknown 40150cd66d update
2026-03-21 07:26:38 +09:00

996 lines
43 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'
]
# 로그 간소화: 전체 펌웨어 항목 수 출력 제거
# 각 펌웨어 항목 조회
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:
pass # 로그 간소화: 진행률 출력 제거
except Exception as e:
print(f"펌웨어 항목 조회 오류 ({idx}): {str(e)}")
continue
# 이름순 정렬
inventory.sort(key=lambda x: x['Name'])
# 로그 간소화: 중요 펌웨어 항목 수 출력 제거
# 주요 컴포넌트 요약
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:
pass # 로그 간소화: BIOS 버전 출력 제거
if idrac:
pass # 로그 간소화: iDRAC 버전 출력 제거
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)
def get_bios_attributes_all(self):
"""
설정된 모든 BIOS 속성 조회
Returns:
dict: {AttributeName: Value, ...}
"""
try:
# 1. BIOS 리소스 조회
url = f"{self.base_url}/redfish/v1/Systems/System.Embedded.1/Bios"
response = self.session.get(url, timeout=15)
if response.status_code == 200:
data = response.json()
# Attributes가 바로 있는 경우 (표준)
if 'Attributes' in data:
return data['Attributes']
# Future: Attributes가 별도 링크인 경우 처리 필요할 수 있음
return {}
else:
print(f"BIOS 속성 조회 실패: {response.status_code}")
return {}
except Exception as e:
print(f"BIOS 속성 조회 오류: {str(e)}")
return {}
def get_bios_attribute_registry(self):
"""
BIOS Attribute Registry를 조회하여 속성 이름(AttributeName)과
사용자 친화적인 이름(DisplayName)의 매핑 정보를 반환합니다.
Returns:
dict: {AttributeName: DisplayName, ...}
"""
try:
# 1. BIOS 리소스에서 AttributeRegistry 정보 확인
url = f"{self.base_url}/redfish/v1/Systems/System.Embedded.1/Bios"
response = self.session.get(url, timeout=15)
if response.status_code != 200:
print("BIOS 리소스 조회 실패")
return {}
data = response.json()
registry_name = data.get('AttributeRegistry')
if not registry_name:
print("BIOS Attribute Registry 정보를 찾을 수 없음. 기본값으로 시도합니다.")
registry_name = "BiosAttributeRegistry"
print(f"DEBUG: Looking for BIOS Registry: {registry_name}")
# 2. Registries 컬렉션에서 해당 레지스트리 찾기
reg_url = f"{self.base_url}/redfish/v1/Registries"
reg_res = self.session.get(reg_url, timeout=10)
target_location = None
if reg_res.status_code == 200:
reg_data = reg_res.json()
# 검색할 이름 후보군 (버전 제거 등)
search_candidates = [registry_name]
if '.' in registry_name:
search_candidates.append(registry_name.split('.')[0])
# 'BiosAttributeRegistry'가 없다면 추가 (하드코딩 폴백)
if 'BiosAttributeRegistry' not in search_candidates:
search_candidates.append('BiosAttributeRegistry')
print(f"DEBUG: Registry Candidates: {search_candidates}")
for member in reg_data.get('Members', []):
m_url = member.get('@odata.id', '')
# URL 끝부분(ID) 추출
# /redfish/v1/Registries/BiosAttributeRegistry
m_id = m_url.rstrip('/').split('/')[-1]
match_found = False
for candidate in search_candidates:
if candidate.lower() in m_id.lower():
match_found = True
break
if match_found:
print(f"DEBUG: Found Registry Match: {m_url}")
# 레지스트리 상세 조회
detail_url = f"{self.base_url}{m_url}"
detail_res = self.session.get(detail_url, timeout=10)
if detail_res.status_code == 200:
detail_data = detail_res.json()
# Location 정보 찾기
locations = detail_data.get('Location', [])
for loc in locations:
if loc.get('Language', '').startswith('en'):
target_location = loc.get('Uri')
break
if not target_location and locations:
target_location = locations[0].get('Uri')
if target_location:
break
# 3. Registry 파일(JSON) 다운로드 및 파싱
if target_location:
# URI가 전체 경로가 아닐 수 있음
if not target_location.startswith('http'):
final_url = f"{self.base_url}{target_location}"
else:
final_url = target_location
print(f"Downloading BIOS Registry from: {final_url}")
file_res = self.session.get(final_url, timeout=30)
if file_res.status_code == 200:
reg_json = file_res.json()
mapping = {}
# RegistryEntries 파싱
# 구조: { "RegistryEntries": { "Attributes": [ ... ] } } 또는 바로 Attributes
attributes = []
if 'RegistryEntries' in reg_json:
if 'Attributes' in reg_json['RegistryEntries']:
attributes = reg_json['RegistryEntries']['Attributes']
else:
# 썸네일 등 다른 구조일 수 있음, 여기서는 일반적 구조 따름
pass
elif 'Attributes' in reg_json:
attributes = reg_json['Attributes']
for entry in attributes:
attr_name = entry.get('AttributeName')
display_name = entry.get('DisplayName')
if attr_name and display_name:
mapping[attr_name] = display_name
print(f"BIOS Registry 파싱 완료: {len(mapping)}개 항목")
return mapping
print(f"BIOS Registry 위치를 찾을 수 없음 (TargetLocation: {target_location})")
return {}
except Exception as e:
print(f"BIOS Attribute Registry 조회 오류: {str(e)}")
import traceback
traceback.print_exc()
return {}
def _flatten_dict(self, d, parent_key='', sep='.'):
"""중첩된 딕셔너리를 평탄화 (Nested JSON -> Flat JSON)"""
items = []
for k, v in d.items():
new_key = f"{parent_key}{sep}{k}" if parent_key else k
if isinstance(v, dict):
items.extend(self._flatten_dict(v, new_key, sep=sep).items())
elif isinstance(v, list):
# 리스트인 경우 인덱스를 붙여서 평탄화
for i, item in enumerate(v):
if isinstance(item, dict):
items.extend(self._flatten_dict(item, f"{new_key}{sep}{i}", sep=sep).items())
else:
items.append((f"{new_key}{sep}{i}", item))
else:
items.append((new_key, v))
return dict(items)
def get_idrac_attributes_all(self):
"""
설정된 모든 iDRAC 속성 조회 (Attributes + NetworkProtocol + AccountService)
Returns:
dict: {AttributeName: Value, ...} (Flattened)
"""
all_attributes = {}
try:
# 1. 기본 Attributes 조회
url = f"{self.base_url}/redfish/v1/Managers/iDRAC.Embedded.1/Attributes"
response = self.session.get(url, timeout=15)
if response.status_code == 200:
data = response.json()
attrs = data.get('Attributes', data)
# @odata 필드 제거
attrs = {k: v for k, v in attrs.items() if not k.startswith('@odata')}
all_attributes.update(attrs)
# 2. Network Protocol 조회 (NTP, SNMP, IPMI, SSH 등)
net_url = f"{self.base_url}/redfish/v1/Managers/iDRAC.Embedded.1/NetworkProtocol"
net_res = self.session.get(net_url, timeout=10)
if net_res.status_code == 200:
net_data = net_res.json()
# 불필요한 필드 제거
exclude_keys = ['@odata.context', '@odata.id', '@odata.type', 'Name', 'Description', 'Id', 'Status']
filtered_net = {k: v for k, v in net_data.items() if k not in exclude_keys}
# 평탄화하여 병합 (Prefix: NetworkProtocol)
flat_net = self._flatten_dict(filtered_net, parent_key='NetworkProtocol')
all_attributes.update(flat_net)
# 3. Account Service 조회 (LDAP, ActiveDirectory)
# /redfish/v1/AccountService
acc_url = f"{self.base_url}/redfish/v1/AccountService"
acc_res = self.session.get(acc_url, timeout=10)
if acc_res.status_code == 200:
acc_data = acc_res.json()
exclude_keys = ['@odata.context', '@odata.id', '@odata.type', 'Name', 'Description', 'Id', 'Status', 'Roles', 'Accounts']
filtered_acc = {k: v for k, v in acc_data.items() if k not in exclude_keys}
flat_acc = self._flatten_dict(filtered_acc, parent_key='AccountService')
all_attributes.update(flat_acc)
return all_attributes
return all_attributes
except Exception as e:
print(f"iDRAC 속성 조회 오류: {str(e)}")
return all_attributes or {}
def get_idrac_attribute_registry(self):
"""
iDRAC Attribute Registry를 조회하여 속성 이름(AttributeName)과
사용자 친화적인 이름(DisplayName)의 매핑 정보를 반환합니다.
Returns:
dict: {AttributeName: DisplayName, ...}
"""
try:
# 1. iDRAC 리소스에서 AttributeRegistry 정보 확인
url = f"{self.base_url}/redfish/v1/Managers/iDRAC.Embedded.1/Attributes"
response = self.session.get(url, timeout=15)
if response.status_code != 200:
print("iDRAC 리소스 조회 실패")
return {}
data = response.json()
registry_name = data.get('AttributeRegistry')
if not registry_name:
print("iDRAC Attribute Registry 정보를 찾을 수 없음. 기본값으로 시도합니다.")
# iDRAC의 경우 보통 'iDRACRegistry' 또는 'IdracAttributeRegistry' 등일 수 있음
# 하지만 대부분의 Redfish 구현체에서 AttributeRegistry 프로퍼티가 있거나
# Registries 컬렉션에서 찾을 수 있습니다.
registry_name = "iDRACAttributeRegistry"
print(f"DEBUG: Looking for iDRAC Registry: {registry_name}")
# 2. Registries 컬렉션에서 해당 레지스트리 찾기
reg_url = f"{self.base_url}/redfish/v1/Registries"
reg_res = self.session.get(reg_url, timeout=10)
target_location = None
if reg_res.status_code == 200:
reg_data = reg_res.json()
# 검색할 이름 후보군 (버전 제거 등)
search_candidates = [registry_name]
if '.' in registry_name:
search_candidates.append(registry_name.split('.')[0])
# 'iDRACAttributeRegistry'가 없다면 추가 (하드코딩 폴백)
if 'iDRACAttributeRegistry' not in search_candidates:
search_candidates.append('iDRACAttributeRegistry')
if 'iDRACRegistry' not in search_candidates:
search_candidates.append('iDRACRegistry')
print(f"DEBUG: iDRAC Registry Candidates: {search_candidates}")
for member in reg_data.get('Members', []):
m_url = member.get('@odata.id', '')
m_id = m_url.rstrip('/').split('/')[-1]
match_found = False
for candidate in search_candidates:
if candidate.lower() in m_id.lower():
match_found = True
break
if match_found:
print(f"DEBUG: Found iDRAC Registry Match: {m_url}")
# 레지스트리 상세 조회
detail_url = f"{self.base_url}{m_url}"
detail_res = self.session.get(detail_url, timeout=10)
if detail_res.status_code == 200:
detail_data = detail_res.json()
# Location 정보 찾기 (영어 버전 선호)
locations = detail_data.get('Location', [])
for loc in locations:
if loc.get('Language', '').startswith('en'):
target_location = loc.get('Uri')
break
if not target_location and locations:
target_location = locations[0].get('Uri')
if target_location:
break
# 3. Registry 파일(JSON) 다운로드 및 파싱
if target_location:
# URI가 전체 경로가 아닐 수 있음
if not target_location.startswith('http'):
final_url = f"{self.base_url}{target_location}"
else:
final_url = target_location
print(f"Downloading iDRAC Registry from: {final_url}")
file_res = self.session.get(final_url, timeout=30)
if file_res.status_code == 200:
reg_json = file_res.json()
mapping = {}
# RegistryEntries 파싱
attributes = []
if 'RegistryEntries' in reg_json:
if 'Attributes' in reg_json['RegistryEntries']:
attributes = reg_json['RegistryEntries']['Attributes']
else:
pass
elif 'Attributes' in reg_json:
attributes = reg_json['Attributes']
for entry in attributes:
attr_name = entry.get('AttributeName')
display_name = entry.get('DisplayName')
if attr_name and display_name:
mapping[attr_name] = display_name
print(f"iDRAC Registry 파싱 완료: {len(mapping)}개 항목")
return mapping
print(f"iDRAC Registry 위치를 찾을 수 없음 (TargetLocation: {target_location})")
return {}
except Exception as e:
print(f"iDRAC Attribute Registry 조회 오류: {str(e)}")
return {}
def get_system_info_detailed(self):
"""
시스템 상세 정보 (CPU, Memory, Model 등) 조회
"""
try:
url = f"{self.base_url}/redfish/v1/Systems/System.Embedded.1"
response = self.session.get(url, timeout=15)
if response.status_code != 200:
return None
data = response.json()
info = {
'Model': data.get('Model', 'Unknown'),
'Manufacturer': data.get('Manufacturer', 'Unknown'),
'SerialNumber': data.get('SerialNumber', 'Unknown'),
'PowerState': data.get('PowerState', 'Unknown'),
'BiosVersion': data.get('BiosVersion', 'Unknown'),
'ProcessorSummary': {
'Count': data.get('ProcessorSummary', {}).get('Count', 0),
'Model': data.get('ProcessorSummary', {}).get('Model', 'Unknown'),
'Status': data.get('ProcessorSummary', {}).get('Status', {}).get('Health', 'Unknown')
},
'MemorySummary': {
'TotalSystemMemoryGiB': data.get('MemorySummary', {}).get('TotalSystemMemoryGiB', 0),
'Status': data.get('MemorySummary', {}).get('Status', {}).get('Health', 'Unknown')
},
'HostName': data.get('HostName', '')
}
return info
except Exception as e:
print(f"시스템 상세 조회 오류: {str(e)}")
return None
def get_storage_configuration(self):
"""
RAID 컨트롤러 및 스토리지 구성 정보 조회
Controllers, Volumes(RAID), Drives 정보 포함
"""
try:
url = f"{self.base_url}/redfish/v1/Systems/System.Embedded.1/Storage"
response = self.session.get(url, timeout=20)
if response.status_code != 200:
print(f"스토리지 조회 실패: {response.status_code}")
return {'Controllers': []}
data = response.json()
controllers = []
for member in data.get('Members', []):
try:
ctrl_url = f"{self.base_url}{member['@odata.id']}"
ctrl_res = self.session.get(ctrl_url, timeout=10)
if ctrl_res.status_code != 200:
continue
ctrl_data = ctrl_res.json()
# 1. 컨트롤러 기본 정보
controller = {
'Name': ctrl_data.get('Name', 'Unknown'),
'Id': ctrl_data.get('Id', ''),
'Model': ctrl_data.get('Model', ''),
'FirmwareVersion': '', # 아래에서 찾음
'Status': ctrl_data.get('Status', {}).get('Health', 'Unknown'),
'Volumes': [],
'Drives': []
}
# 펌웨어 버전 찾기 (Controllers 리스트 내)
for c in ctrl_data.get('Controllers', []):
if 'FirmwareVersion' in c:
controller['FirmwareVersion'] = c['FirmwareVersion']
break
# 2. Volumes (Virtual Disks)
# Volumes가 링크로 되어있는 경우와 바로 있는 경우가 있음
volumes_url = f"{ctrl_url}/Volumes"
vol_res = self.session.get(volumes_url, timeout=10)
if vol_res.status_code == 200:
vol_data = vol_res.json()
for vol_member in vol_data.get('Members', []):
v_url = f"{self.base_url}{vol_member['@odata.id']}"
v_res = self.session.get(v_url, timeout=10)
if v_res.status_code == 200:
v_data = v_res.json()
controller['Volumes'].append({
'Name': v_data.get('Name', ''),
'RaidType': v_data.get('RAIDType', 'Unknown'), # RAID0, RAID1, etc
'CapacityBytes': v_data.get('CapacityBytes', 0),
'BlockSizeBytes': v_data.get('BlockSizeBytes', 0),
'Status': v_data.get('Status', {}).get('Health', 'Unknown')
})
# 3. Drives (Physical Disks)
drives_url = f"{ctrl_url}/Drives"
drv_res = self.session.get(drives_url, timeout=10)
if drv_res.status_code == 200:
drv_data = drv_res.json()
for drv_member in drv_data.get('Members', []):
d_url = f"{self.base_url}{drv_member['@odata.id']}"
d_res = self.session.get(d_url, timeout=10)
if d_res.status_code == 200:
d_data = d_res.json()
controller['Drives'].append({
'Name': d_data.get('Name', ''),
'Model': d_data.get('Model', ''),
'Protocol': d_data.get('Protocol', 'Unknown'), # SATA, SAS, NVMe
'MediaType': d_data.get('MediaType', 'Unknown'), # HDD, SSD
'CapacityBytes': d_data.get('CapacityBytes', 0),
'Status': d_data.get('Status', {}).get('Health', 'Unknown')
})
controllers.append(controller)
except Exception as e:
print(f"컨트롤러 상세 조회 오류 ({member.get('@odata.id')}): {str(e)}")
return {'Controllers': controllers}
except Exception as e:
print(f"스토리지 구성 조회 오류: {str(e)}")
return {'Controllers': []}
def get_nic_configuration(self):
"""
NIC 구성 정보 조회 (NetworkAdapters + DeviceFunctions)
"""
try:
url = f"{self.base_url}/redfish/v1/Systems/System.Embedded.1/NetworkAdapters"
response = self.session.get(url, timeout=15)
if response.status_code != 200:
print(f"NIC 어댑터 목록 조회 실패: {response.status_code}")
return {'Adapters': []}
data = response.json()
adapters = []
for member in data.get('Members', []):
try:
# 어댑터 기본 정보
adp_url = f"{self.base_url}{member['@odata.id']}"
adp_res = self.session.get(adp_url, timeout=10)
if adp_res.status_code != 200:
continue
adp_data = adp_res.json()
adapter = {
'Id': adp_data.get('Id', ''),
'Name': adp_data.get('Name', 'Unknown'),
'Manufacturer': adp_data.get('Manufacturer', ''),
'Model': adp_data.get('Model', ''),
'SerialNumber': adp_data.get('SerialNumber', ''),
'FirmwareVersion': '', # LinkStatus 등은 Port에 있음
'DeviceFunctions': []
}
# 펌웨어 버전 추출
if 'FirmwarePackageVersion' in adp_data:
adapter['FirmwareVersion'] = adp_data['FirmwarePackageVersion']
# NetworkDeviceFunctions (iSCSI, PXE 등 설정)
funcs_url = f"{adp_url}/NetworkDeviceFunctions"
funcs_res = self.session.get(funcs_url, timeout=10)
if funcs_res.status_code == 200:
funcs_data = funcs_res.json()
for f_mem in funcs_data.get('Members', []):
f_url = f"{self.base_url}{f_mem['@odata.id']}"
f_res = self.session.get(f_url, timeout=10)
if f_res.status_code == 200:
f_data = f_res.json()
# Dell 특화 Attributes 또는 표준 Redfish 속성
func_info = {
'Id': f_data.get('Id', ''),
'Name': f_data.get('Name', ''),
'NetBootPMMode': f_data.get('BootMode', 'Unknown'), # Legacy, UEFI
'iSCSIBoot': {},
'Ethernet': {}
}
# iSCSI Boot 속성 평탄화
if 'iSCSIBoot' in f_data:
func_info['iSCSIBoot'] = f_data['iSCSIBoot']
# 이더넷 속성 (MAC 등)
if 'Ethernet' in f_data:
func_info['Ethernet'] = {
'MACAddress': f_data['Ethernet'].get('MACAddress', ''),
'MTUSize': f_data['Ethernet'].get('MTUSize', '')
}
adapter['DeviceFunctions'].append(func_info)
adapters.append(adapter)
except Exception as e:
print(f"NIC 상세 조회 오류: {str(e)}")
return {'Adapters': adapters}
except Exception as e:
print(f"NIC 구성 조회 오류: {str(e)}")
return {'Adapters': []}
def get_boot_settings(self):
"""
부팅 설정 조회 (Boot Order, Boot Source Override)
"""
try:
url = f"{self.base_url}/redfish/v1/Systems/System.Embedded.1"
response = self.session.get(url, timeout=15)
if response.status_code == 200:
data = response.json()
boot = data.get('Boot', {})
return {
'BootOrder': boot.get('BootOrder', []),
'BootSourceOverrideTarget': boot.get('BootSourceOverrideTarget', 'None'),
'BootSourceOverrideMode': boot.get('BootSourceOverrideMode', 'Legacy'),
'BootSourceOverrideEnabled': boot.get('BootSourceOverrideEnabled', 'Disabled')
}
return {}
except Exception as e:
print(f"부팅 설정 조회 오류: {str(e)}")
return {}
def get_firmware_baseline(self):
"""
Baseline 비교용 펌웨어 목록 (간소화)
"""
try:
inventory = self.get_firmware_inventory()
baseline_fw = {}
for item in inventory:
# 키: 컴포넌트 이름 (중복 시 타입/ID 병기하여 유니크하게 만들 필요 있음)
# 여기서는 단순화를 위해 Name을 Key로 사용하되, 중복되면 덮어씀 (일반적으로 주요 컴포넌트는 유니크함)
# 또는 {ComponentType}_{Name} 형식 추천
key = item['Name']
baseline_fw[key] = item['Version']
return baseline_fw
except Exception as e:
print(f"펌웨어 Baseline 조회 오류: {str(e)}")
return {}