Initial project upload
This commit is contained in:
@@ -0,0 +1,995 @@
|
||||
"""
|
||||
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 {}
|
||||
Reference in New Issue
Block a user