update
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -66,7 +66,7 @@ class DellRedfishClient:
|
||||
'Mellanox', 'Emulex', 'QLogic'
|
||||
]
|
||||
|
||||
print(f"전체 펌웨어 항목 수: {len(data.get('Members', []))}")
|
||||
# 로그 간소화: 전체 펌웨어 항목 수 출력 제거
|
||||
|
||||
# 각 펌웨어 항목 조회
|
||||
for idx, member in enumerate(data.get('Members', [])):
|
||||
@@ -110,7 +110,7 @@ class DellRedfishClient:
|
||||
|
||||
# 진행 상황 출력
|
||||
if (idx + 1) % 10 == 0:
|
||||
print(f"조회 중... {idx + 1}/{len(data.get('Members', []))}")
|
||||
pass # 로그 간소화: 진행률 출력 제거
|
||||
|
||||
except Exception as e:
|
||||
print(f"펌웨어 항목 조회 오류 ({idx}): {str(e)}")
|
||||
@@ -119,16 +119,16 @@ class DellRedfishClient:
|
||||
# 이름순 정렬
|
||||
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']}")
|
||||
pass # 로그 간소화: BIOS 버전 출력 제거
|
||||
if idrac:
|
||||
print(f"✓ iDRAC 버전: {idrac['Version']}")
|
||||
pass # 로그 간소화: iDRAC 버전 출력 제거
|
||||
|
||||
return inventory
|
||||
|
||||
@@ -395,3 +395,601 @@ class DellRedfishClient:
|
||||
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 {}
|
||||
|
||||
@@ -54,8 +54,16 @@ def save_ip_addresses(ips: str, folder: str | os.PathLike[str]) -> list[tuple[st
|
||||
# 개별 IP 처리
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _build_command(script: str, ip_file: str, xml_file: str | None) -> list[str]:
|
||||
script_path = Path(Config.SCRIPT_FOLDER) / script
|
||||
def _build_command(script: str, ip_file: str, xml_file: str | None, profile_name: str | None = None) -> list[str]:
|
||||
# unified 스크립트는 data/scripts/unified 폴더에 있음
|
||||
# 기존 레거시 스크립트는 data/scripts 폴더에 있음
|
||||
# script 인자가 "unified/..." 로 시작하면 해당 경로 사용, 아니면 data/scripts 기준
|
||||
|
||||
if script.startswith("unified/"):
|
||||
script_path = Path(Config.SCRIPT_FOLDER) / script
|
||||
else:
|
||||
script_path = Path(Config.SCRIPT_FOLDER) / script
|
||||
|
||||
if not script_path.exists():
|
||||
raise FileNotFoundError(f"스크립트를 찾을 수 없습니다: {script_path}")
|
||||
|
||||
@@ -73,14 +81,23 @@ def _build_command(script: str, ip_file: str, xml_file: str | None) -> list[str]
|
||||
else:
|
||||
raise ValueError(f"지원되지 않는 스크립트 형식: {script_path.suffix}")
|
||||
|
||||
# 프로파일이 있으면 추가 (unified 스크립트용)
|
||||
if profile_name:
|
||||
cmd.append(profile_name)
|
||||
|
||||
if xml_file:
|
||||
cmd.append(xml_file)
|
||||
return cmd
|
||||
|
||||
|
||||
def process_ip(ip_file: str, script: str, xml_file: str | None = None) -> None:
|
||||
def process_ip(ip_file: str, script: str, xml_file: str | None = None, profile_name: str | None = None, slot_priority: str | None = None) -> None:
|
||||
ip = Path(ip_file).read_text(encoding="utf-8").strip()
|
||||
cmd = _build_command(script, ip_file, xml_file)
|
||||
cmd = _build_command(script, ip_file, xml_file, profile_name)
|
||||
|
||||
# 환경 변수 설정 (기존 환경 변수 복사 + 슬롯 우선순위 추가)
|
||||
env = os.environ.copy()
|
||||
if slot_priority:
|
||||
env["GUID_SLOT_PRIORITY"] = slot_priority
|
||||
|
||||
logging.info("🔧 실행 명령: %s", " ".join(cmd))
|
||||
try:
|
||||
@@ -91,6 +108,7 @@ def process_ip(ip_file: str, script: str, xml_file: str | None = None) -> None:
|
||||
check=True,
|
||||
cwd=str(Path(Config.SCRIPT_FOLDER)),
|
||||
timeout=int(os.getenv("SCRIPT_TIMEOUT", "1800")), # 30분 기본
|
||||
env=env,
|
||||
)
|
||||
logging.info("[%s] ✅ stdout:\n%s", ip, result.stdout)
|
||||
if result.stderr:
|
||||
@@ -105,14 +123,14 @@ def process_ip(ip_file: str, script: str, xml_file: str | None = None) -> None:
|
||||
# 병렬 처리 진입점
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
def process_ips_concurrently(ip_files, job_id, observer: Observer, script: str, xml_file: str | None):
|
||||
def process_ips_concurrently(ip_files, job_id, observer: Observer, script: str, xml_file: str | None, profile_name: str | None = None, slot_priority: str | None = None):
|
||||
total = len(ip_files)
|
||||
completed = 0
|
||||
_set_progress(job_id, 0)
|
||||
|
||||
try:
|
||||
with ThreadPoolExecutor(max_workers=Config.MAX_WORKERS) as pool:
|
||||
futures = {pool.submit(process_ip, ip_path, script, xml_file): ip for ip, ip_path in ip_files}
|
||||
futures = {pool.submit(process_ip, ip_path, script, xml_file, profile_name, slot_priority): ip for ip, ip_path in ip_files}
|
||||
for fut in as_completed(futures):
|
||||
ip = futures[fut]
|
||||
try:
|
||||
@@ -135,7 +153,7 @@ def process_ips_concurrently(ip_files, job_id, observer: Observer, script: str,
|
||||
# 외부에서 한 번에 처리(동기)
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
def handle_ip_processing(ip_text: str, script: str, xml_file: str | None = None) -> str:
|
||||
def handle_ip_processing(ip_text: str, script: str, xml_file: str | None = None, profile_name: str | None = None) -> str:
|
||||
job_id = str(uuid.uuid4())
|
||||
|
||||
temp_dir = Path(Config.UPLOAD_FOLDER) / job_id
|
||||
@@ -148,5 +166,5 @@ def handle_ip_processing(ip_text: str, script: str, xml_file: str | None = None)
|
||||
observer.schedule(handler, Config.IDRAC_INFO_FOLDER, recursive=False)
|
||||
observer.start()
|
||||
|
||||
process_ips_concurrently(ip_files, job_id, observer, script, xml_path)
|
||||
process_ips_concurrently(ip_files, job_id, observer, script, xml_path, profile_name, None)
|
||||
return job_id
|
||||
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
텔레그램 봇 폴링 서비스
|
||||
- 백그라운드에서 텔레그램 봇의 업데이트를 폴링
|
||||
- 인라인 버튼 클릭 처리 (가입 승인/거부)
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from telegram import Update
|
||||
from telegram.ext import Application, CallbackQueryHandler, ContextTypes
|
||||
from flask import Flask
|
||||
|
||||
from backend.models.telegram_bot import TelegramBot
|
||||
from backend.models.user import User, db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def handle_approval_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""
|
||||
텔레그램 인라인 버튼 클릭 처리
|
||||
callback_data 형식: "approve_{token}" 또는 "reject_{token}"
|
||||
"""
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
|
||||
data = query.data or ""
|
||||
logger.info("Received callback: %s", data)
|
||||
|
||||
# Flask app 객체는 bot_data에 저장해둔 것을 사용
|
||||
flask_app: Optional[Flask] = context.application.bot_data.get("flask_app")
|
||||
|
||||
if flask_app is None:
|
||||
logger.error("Flask app context is missing in bot_data")
|
||||
await query.edit_message_text(
|
||||
text="❌ 내부 설정 오류로 요청을 처리할 수 없습니다. 관리자에게 문의해 주세요."
|
||||
)
|
||||
return
|
||||
|
||||
# callback_data 형식 검증
|
||||
if "_" not in data:
|
||||
logger.warning("Invalid callback data format: %s", data)
|
||||
await query.edit_message_text(
|
||||
text="❌ 유효하지 않은 요청입니다."
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
action, token = data.split("_", 1)
|
||||
except ValueError:
|
||||
logger.warning("Failed to split callback data: %s", data)
|
||||
await query.edit_message_text(
|
||||
text="❌ 유효하지 않은 요청입니다."
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
with flask_app.app_context():
|
||||
# 토큰으로 사용자 찾기
|
||||
user = User.query.filter_by(approval_token=token).first()
|
||||
|
||||
if not user:
|
||||
await query.edit_message_text(
|
||||
text="❌ 유효하지 않은 승인 요청입니다.\n(이미 처리되었거나 만료된 요청)"
|
||||
)
|
||||
return
|
||||
|
||||
if action == "approve":
|
||||
# 승인 처리
|
||||
user.is_approved = True
|
||||
user.is_active = True
|
||||
user.approval_token = None # 토큰 무효화
|
||||
|
||||
db.session.commit()
|
||||
|
||||
await query.edit_message_text(
|
||||
text=(
|
||||
"✅ 승인 완료!\n\n"
|
||||
f"👤 사용자: {user.username}\n"
|
||||
f"📧 이메일: {user.email}\n\n"
|
||||
"사용자가 이제 로그인할 수 있습니다."
|
||||
)
|
||||
)
|
||||
logger.info("User %s approved", user.username)
|
||||
|
||||
elif action == "reject":
|
||||
# 거부 처리 - 사용자 삭제
|
||||
username = user.username
|
||||
email = user.email
|
||||
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
|
||||
await query.edit_message_text(
|
||||
text=(
|
||||
"❌ 가입 거부됨\n\n"
|
||||
f"👤 사용자: {username}\n"
|
||||
f"📧 이메일: {email}\n\n"
|
||||
"계정이 삭제되었습니다."
|
||||
)
|
||||
)
|
||||
logger.info("User %s rejected and deleted", username)
|
||||
else:
|
||||
logger.warning("Unknown action in callback: %s", action)
|
||||
await query.edit_message_text(
|
||||
text="❌ 유효하지 않은 요청입니다."
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Error handling callback: %s", e)
|
||||
# 예외 내용은 사용자에게 직접 노출하지 않음
|
||||
try:
|
||||
db.session.rollback()
|
||||
except Exception:
|
||||
logger.exception("DB rollback failed")
|
||||
|
||||
await query.edit_message_text(
|
||||
text="❌ 요청 처리 중 오류가 발생했습니다. 잠시 후 다시 시도하거나 관리자에게 문의해 주세요."
|
||||
)
|
||||
|
||||
|
||||
def run_polling(flask_app: Flask) -> None:
|
||||
"""
|
||||
동기 함수: 백그라운드 스레드에서 직접 호출됨
|
||||
Application.run_polling() 이 내부에서 asyncio 이벤트 루프를 관리하므로
|
||||
여기서는 asyncio.run 을 사용하지 않는다.
|
||||
"""
|
||||
if flask_app is None:
|
||||
raise ValueError("flask_app is required for run_polling")
|
||||
|
||||
bot_token: Optional[str] = None
|
||||
bot_name: Optional[str] = None
|
||||
bot_id: Optional[int] = None
|
||||
|
||||
# DB에서 활성 봇 조회
|
||||
with flask_app.app_context():
|
||||
bots = TelegramBot.query.filter_by(is_active=True).all()
|
||||
|
||||
if not bots:
|
||||
logger.warning("No active bots found for polling service.")
|
||||
return
|
||||
|
||||
if len(bots) > 1:
|
||||
logger.warning("Multiple active bots found. Only the first one (%s) will be used.", bots[0].name)
|
||||
|
||||
# 첫 번째 활성 봇 사용
|
||||
bot = bots[0]
|
||||
# DB 세션 밖에서도 사용할 수 있도록 필요한 정보만 추출 (Detached Instance 에러 방지)
|
||||
bot_token = bot.token
|
||||
bot_name = bot.name
|
||||
bot_id = bot.id
|
||||
|
||||
logger.info("Starting polling for bot: %s (ID: %s)", bot_name, bot_id)
|
||||
|
||||
if not bot_token:
|
||||
logger.error("Bot token not found.")
|
||||
return
|
||||
|
||||
# Application 생성
|
||||
application = Application.builder().token(bot_token).build()
|
||||
|
||||
# Flask app을 bot_data에 넣어서 핸들러에서 사용할 수 있게 함
|
||||
application.bot_data["flask_app"] = flask_app
|
||||
|
||||
# 콜백 쿼리 핸들러 등록
|
||||
application.add_handler(CallbackQueryHandler(handle_approval_callback))
|
||||
|
||||
try:
|
||||
# v20 스타일: run_polling 은 동기 함수이고, 내부에서 이벤트 루프를 직접 관리함
|
||||
application.run_polling(drop_pending_updates=True, stop_signals=[])
|
||||
except Exception as e:
|
||||
logger.exception("Error in bot polling: %s", e)
|
||||
Reference in New Issue
Block a user