Initial project upload

This commit is contained in:
Kim.KANGHEE
2026-05-05 17:14:11 +09:00
commit 122b73d254
282 changed files with 72135 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
"""
공통 라이브러리 패키지
"""
__version__ = "1.0.0"
+129
View File
@@ -0,0 +1,129 @@
"""
iDRAC 클라이언트 - racadm 명령 실행 및 결과 캐싱
"""
from typing import Optional, Dict, Tuple
import subprocess
import os
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [INFO] root: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
class IDRACClient:
"""
racadm 명령 실행 및 결과 캐싱을 담당하는 클라이언트
"""
def __init__(self, ip: str, user: str = None, password: str = None):
"""
Args:
ip: iDRAC IP 주소
user: iDRAC 사용자명 (기본값: 환경변수 IDRAC_USER 또는 'root')
password: iDRAC 비밀번호 (기본값: 환경변수 IDRAC_PASS 또는 'calvin')
"""
self.ip = ip
self.user = user or os.getenv("IDRAC_USER", "root")
self.password = password or os.getenv("IDRAC_PASS", "calvin")
self._cache: Dict[str, str] = {} # 중복 호출 방지 캐시
def run_command(self, command: str) -> Tuple[int, str]:
"""
racadm 명령 실행
Args:
command: 실행할 racadm 명령 (예: "getsysinfo", "get bios.BiosBootSettings")
Returns:
(return_code, output) 튜플
"""
cmd = f"racadm -r {self.ip} -u {self.user} -p {self.password} {command}"
try:
result = subprocess.run(
cmd,
shell=True,
capture_output=True,
text=True,
check=False,
timeout=30 # 30초 타임아웃
)
return result.returncode, result.stdout
except subprocess.TimeoutExpired:
logging.error(f"[{self.ip}] 명령 타임아웃: {command}")
return 1, ""
except Exception as e:
logging.error(f"[{self.ip}] 명령 실행 오류: {e}")
return 1, f"Error: {e}"
def get_sysinfo(self) -> str:
"""
getsysinfo 명령 실행 (캐싱)
Returns:
getsysinfo 출력 결과
"""
if "sysinfo" not in self._cache:
_, output = self.run_command("getsysinfo")
self._cache["sysinfo"] = output
return self._cache["sysinfo"]
def get_hwinventory(self) -> str:
"""
hwinventory 명령 실행 (캐싱)
Returns:
hwinventory 출력 결과
"""
if "hwinventory" not in self._cache:
_, output = self.run_command("hwinventory")
self._cache["hwinventory"] = output
return self._cache["hwinventory"]
def get_swinventory(self) -> str:
"""
swinventory 명령 실행 (캐싱)
Returns:
swinventory 출력 결과
"""
if "swinventory" not in self._cache:
_, output = self.run_command("swinventory")
self._cache["swinventory"] = output
return self._cache["swinventory"]
def get_infiniband_pages(self) -> str:
"""
InfiniBand 설정 페이지 목록 조회 (InfiniBand.VndrConfigPage)
Returns:
racadm get InfiniBand.VndrConfigPage 출력
"""
if "infiniband_pages" not in self._cache:
_, output = self.run_command("get InfiniBand.VndrConfigPage")
self._cache["infiniband_pages"] = output
return self._cache["infiniband_pages"]
def get_infiniband_page_detail(self, page_number: str) -> str:
"""
특정 InfiniBand 설정 페이지 상세 조회
Args:
page_number: 페이지 번호 (예: "1")
Returns:
racadm get InfiniBand.VndrConfigPage.{page_number} 출력
"""
cache_key = f"infiniband_page_{page_number}"
if cache_key not in self._cache:
_, output = self.run_command(f"get InfiniBand.VndrConfigPage.{page_number}")
self._cache[cache_key] = output
return self._cache[cache_key]
def clear_cache(self):
"""캐시 초기화"""
self._cache.clear()
+297
View File
@@ -0,0 +1,297 @@
"""
racadm 출력 파싱 유틸리티
"""
import re
from typing import List, Optional, Dict, Tuple
class InfoParser:
"""racadm 명령 출력 파싱 유틸리티"""
# Word boundary to ensure we don't match partial GUIDs (which are longer)
MAC_PATTERN = re.compile(r"\b([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}\b")
@staticmethod
def extract_service_tag(sysinfo: str) -> Optional[str]:
"""
서비스 태그 추출
Args:
sysinfo: getsysinfo 명령 출력
Returns:
서비스 태그 또는 None
"""
m = re.search(r"SVC\s*Tag\s*=\s*(\S+)", sysinfo, re.IGNORECASE)
return m.group(1).strip() if m else None
@staticmethod
def extract_nic_macs(sysinfo: str, nic_patterns: List[str]) -> Dict[str, Optional[str]]:
"""
NIC MAC 주소 추출
Args:
sysinfo: getsysinfo 명령 출력
nic_patterns: NIC 패턴 리스트 (예: ["NIC.Integrated.1-1-1", "NIC.Embedded.1-1-1"])
Returns:
{pattern: mac_address} 딕셔너리
"""
results = {}
for pattern in nic_patterns:
for line in sysinfo.splitlines():
if pattern in line:
mac = InfoParser.MAC_PATTERN.search(line)
results[pattern] = mac.group(0) if mac else None
break
else:
results[pattern] = None
return results
@staticmethod
def extract_nic_macs_from_hwinventory(hwinventory: str, nic_patterns: List[str]) -> Dict[str, Optional[str]]:
"""
hwinventory에서 NIC MAC 주소 추출
Args:
hwinventory: hwinventory 명령 출력
nic_patterns: NIC 패턴 리스트 (예: ["NIC.Integrated.1-1-1", "NIC.Slot.1-1-1"])
Returns:
{pattern: mac_address} 딕셔너리
"""
results = {pattern: None for pattern in nic_patterns}
# hwinventory를 블록 단위로 분리 (빈 줄로 구분)
blocks = re.split(r"\n\s*\n", hwinventory)
for block in blocks:
# 블록 내에서 FQDD 또는 InstanceID 확인
fqdd = None
# FQDD = NIC.Integrated.1-1-1 또는 [InstanceID: NIC.Integrated.1-1-1] 형식 찾기
m_fqdd = re.search(r"FQDD\s*=\s*(\S+)", block, re.IGNORECASE)
if m_fqdd:
fqdd = m_fqdd.group(1).strip()
# FQDD 키워드가 없을 경우를 대비해 InstanceID 헤더도 확인
elif "[InstanceID:" in block:
m_inst = re.search(r"InstanceID:\s*([^\]]+)", block)
if m_inst:
fqdd = m_inst.group(1).strip()
if not fqdd:
continue
# 요청된 패턴 중 하나라도 포함하는지 확인 (정확한 매칭을 위해 수정 가능하나, 보통 포함 관계면 충분)
# 여기서는 패턴이 FQDD와 정확히 일치하거나 FQDD가 패턴을 포함하는 경우를 찾음
matched_pattern = None
for pattern in nic_patterns:
# 대소문자 무시 비교
if pattern.lower() == fqdd.lower():
matched_pattern = pattern
break
# 정확히 일치하는 패턴을 못 찾았다면 다음 블록으로
if not matched_pattern:
continue
# MAC 주소 찾기
# Ethernet Address = AA:BB:CC:DD:EE:FF
# CurrentMACAddress = AA:BB:CC:DD:EE:FF
# MAC Address = AA:BB:CC:DD:EE:FF
mac_match = InfoParser.MAC_PATTERN.search(block)
if mac_match:
results[matched_pattern] = mac_match.group(0)
return results
@staticmethod
def extract_idrac_mac(sysinfo: str) -> Optional[str]:
"""
iDRAC MAC 주소 추출
Args:
sysinfo: getsysinfo 명령 출력
Returns:
iDRAC MAC 주소 또는 None
"""
for line in sysinfo.splitlines():
if "MAC Address" in line:
mac = InfoParser.MAC_PATTERN.search(line)
if mac:
return mac.group(0)
return None
@staticmethod
def extract_vendors(hwinventory: str, component_prefix: str, instance_must_contain: str = None) -> List[str]:
"""
컴포넌트 벤더 추출
Args:
hwinventory: hwinventory 명령 출력
component_prefix: 컴포넌트 접두사 ("DIMM", "Disk.Bay", "BOSS" 등)
instance_must_contain: InstanceID/FQDD에 반드시 포함돼야 할 문자열 (예: "BOSS")
Returns:
벤더 리스트 (중복 제거, 정렬됨)
"""
vendors = set()
blocks = re.split(r"\n\s*\n", hwinventory)
for block in blocks:
# Check if this block is for the target component using InstanceID or FQDD
is_target_component = False
matched_id = None
# Check InstanceID header: [InstanceID: DIMM.Socket.A1]
if f"[InstanceID: {component_prefix}" in block:
is_target_component = True
m_inst = re.search(r"\[InstanceID:\s*([^\]]+)\]", block)
if m_inst:
matched_id = m_inst.group(1).strip()
# Also check FQDD line if InstanceID header check fails (fallback)
elif not is_target_component:
m_fqdd = re.search(r"^FQDD\s*=\s*(\S+)", block, re.MULTILINE)
if m_fqdd and component_prefix in m_fqdd.group(1):
is_target_component = True
matched_id = m_fqdd.group(1).strip()
if not is_target_component:
continue
# instance_must_contain 필터 적용
if instance_must_contain and matched_id and instance_must_contain not in matched_id:
continue
m = re.search(r"Manufacturer\s*=\s*(.+)", block, re.IGNORECASE)
if m:
vendors.add(m.group(1).strip())
return sorted(vendors)
@staticmethod
def extract_infiniband_page_map(pages_output: str) -> List[Tuple[str, str]]:
"""
InfiniBand 페이지 번호와 슬롯 번호 매핑 추출
Args:
pages_output: get InfiniBand.VndrConfigPage 출력
Returns:
[(page_number, slot_number), ...] 리스트
"""
# InfiniBand.VndrConfigPage.1 [Key=InfiniBand.Slot.38-1#VndrConfigPage]
matches = re.findall(
r"InfiniBand\.VndrConfigPage\.(\d+)\s+\[Key=InfiniBand\.Slot\.(\d+)-\d+#VndrConfigPage]",
pages_output
)
return matches
@staticmethod
def extract_port_guid(page_detail: str) -> Optional[str]:
"""
InfiniBand 페이지 상세에서 PortGUID 추출
Args:
page_detail: get InfiniBand.VndrConfigPage.{N} 출력
Returns:
PortGUID 값 또는 None
"""
m = re.search(r"PortGUID=(\S+)", page_detail)
return m.group(1) if m else None
@staticmethod
def extract_value_by_pattern(text: str, pattern: str) -> Optional[str]:
"""
패턴으로 값 추출 (= 뒤의 값)
Args:
text: 검색할 텍스트
pattern: 검색 패턴
Returns:
추출된 값 또는 None
"""
for line in text.splitlines():
if pattern.lower() in line.lower():
if '=' in line:
return line.split('=', 1)[1].strip()
return None
@staticmethod
def extract_infiniband_macs_from_swinventory(swinventory: str, target_slots: List[int]) -> Dict[int, str]:
"""
swinventory에서 InfiniBand 슬롯의 MAC 주소 추출
(FQDD 라인 바로 윗줄에 MAC 주소가 있다는 가정 - 레거시 스크립트 로직)
Args:
swinventory: swinventory 명령 출력
target_slots: 추출할 슬롯 번호 리스트
Returns:
{slot: mac_address} 딕셔너리
"""
results = {}
prev_line = ""
# 슬롯별 패턴 미리 생성 (예: "FQDD = InfiniBand.Slot.31-1")
slot_patterns = {slot: f"FQDD = InfiniBand.Slot.{slot}-1" for slot in target_slots}
for line in swinventory.splitlines():
for slot, pattern in slot_patterns.items():
if pattern in line:
# 이전 라인에서 MAC 찾기
mac = InfoParser.MAC_PATTERN.search(prev_line)
if mac:
results[slot] = mac.group(0)
prev_line = line
return results
@staticmethod
def extract_gpu_serials(hwinventory: str) -> Dict[str, str]:
"""
iDRAC hwinventory 전체 텍스트에서 GPU(Video.Slot.*) 블록을 찾아
{FQDD(or InstanceID): SerialNumber} 딕셔너리로 반환.
Args:
hwinventory: hwinventory 명령 출력
Returns:
{fqdd: serial} 딕셔너리
"""
results = {}
# 빈 줄 기준 블록 분할(여러 개의 개행을 하나의 경계로)
blocks = re.split(r"\n\s*\n", hwinventory, flags=re.MULTILINE)
for block in blocks:
# GPU(Video) 블록만 처리
# Video.Slot.X 또는 유사 패턴
if not re.search(r"\[?InstanceID:\s*Video\.Slot\.", block):
continue
# FQDD 우선, 없으면 [InstanceID: ...]에서 추출
fqdd = None
m_fqdd = re.search(r"^FQDD\s*=\s*([^\r\n]+)", block, flags=re.MULTILINE)
if m_fqdd:
fqdd = m_fqdd.group(1).strip()
else:
m_hdr = re.search(r"\[InstanceID:\s*(Video\.Slot\.[^\]\r\n]+)\]", block)
if m_hdr:
fqdd = m_hdr.group(1).strip()
if not fqdd:
continue
# SerialNumber 추출
m_sn = re.search(r"^SerialNumber\s*=\s*([^\r\n]+)", block, flags=re.MULTILINE)
serial = m_sn.group(1).strip() if m_sn else "Not Found"
results[fqdd] = serial
return results