Initial project upload
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
공통 라이브러리 패키지
|
||||
"""
|
||||
__version__ = "1.0.0"
|
||||
@@ -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()
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user