""" 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 {}