update
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
통합 GPU 시리얼 수집 스크립트
|
||||
hwinventory에서 Video.Slot 정보 수집
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import logging
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from core.idrac_client import IDRACClient
|
||||
from core.parsers import InfoParser
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s [INFO] root: %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
def collect_gpu_for_ip(ip: str, output_dir: Path) -> tuple:
|
||||
"""단일 IP에 대한 GPU Serial 정보 수집"""
|
||||
try:
|
||||
logging.info(f"[{ip}] GPU 정보 수집 시작")
|
||||
|
||||
client = IDRACClient(ip)
|
||||
|
||||
# 1. Get Service Tag
|
||||
sysinfo = client.get_sysinfo()
|
||||
svc_tag = InfoParser.extract_service_tag(sysinfo)
|
||||
|
||||
if not svc_tag:
|
||||
return ip, False, "서비스 태그 추출 실패"
|
||||
|
||||
# 2. Get Hardware Inventory
|
||||
hwinventory = client.get_hwinventory()
|
||||
|
||||
# 3. Parse GPU Serials
|
||||
gpu_map = InfoParser.extract_gpu_serials(hwinventory)
|
||||
|
||||
# 4. Write File
|
||||
output_file = output_dir / f"{svc_tag}.txt"
|
||||
|
||||
# 정렬 로직 (Legacy GPU_Serial_v1.py 유지)
|
||||
def slot_key(k: str):
|
||||
m = re.search(r"Video\.Slot\.(\d+)-(\d+)", k)
|
||||
if m:
|
||||
return (int(m.group(1)), int(m.group(2)))
|
||||
return (1_000_000, 1_000_000, k)
|
||||
|
||||
with output_file.open('w', encoding='utf-8', newline='\n') as f:
|
||||
f.write(f"{svc_tag}\n")
|
||||
|
||||
if not gpu_map:
|
||||
f.write("No GPU(Video) inventory found or SerialNumber not present.\n")
|
||||
return ip, True, f"저장: {output_file.name} (GPU 없음)"
|
||||
|
||||
# Write individual slots
|
||||
sorted_keys = sorted(gpu_map.keys(), key=slot_key)
|
||||
for fqdd in sorted_keys:
|
||||
serial = gpu_map[fqdd]
|
||||
f.write(f"{fqdd}: {serial}\n")
|
||||
|
||||
# Write summary line
|
||||
serials_only = [gpu_map[k] for k in sorted_keys if gpu_map[k] != "Not Found"]
|
||||
if serials_only:
|
||||
f.write(f"SERIALS: {';'.join(serials_only)}\n")
|
||||
|
||||
return ip, True, f"저장: {output_file.name} ({len(serials_only)}개 발견)"
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"[{ip}] 오류: {e}")
|
||||
return ip, False, f"오류: {e}"
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python collect_gpu.py <ip_file>")
|
||||
sys.exit(1)
|
||||
|
||||
ip_file = Path(sys.argv[1])
|
||||
|
||||
if not ip_file.exists():
|
||||
logging.error(f"IP 파일 없음: {ip_file}")
|
||||
sys.exit(1)
|
||||
|
||||
# 출력 디렉토리
|
||||
# 출력 디렉토리 (스크립트 위치 기준)
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
data_root = script_dir.parent.parent
|
||||
output_dir = data_root / "temp" / "staging"
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# IP 목록 읽기
|
||||
ips = [line.strip() for line in ip_file.read_text(encoding='utf-8').splitlines() if line.strip()]
|
||||
|
||||
if not ips:
|
||||
logging.error("IP 목록이 비어 있습니다.")
|
||||
sys.exit(1)
|
||||
|
||||
logging.info(f"총 {len(ips)}대 GPU 정보 수집 시작")
|
||||
|
||||
# 병렬 처리
|
||||
ok, fail = 0, 0
|
||||
with ThreadPoolExecutor(max_workers=20) as executor:
|
||||
futures = {executor.submit(collect_gpu_for_ip, ip, output_dir): ip for ip in ips}
|
||||
|
||||
for future in futures:
|
||||
ip, success, msg = future.result()
|
||||
if success:
|
||||
logging.info(f"[OK] {ip} - {msg}")
|
||||
ok += 1
|
||||
else:
|
||||
logging.error(f"[FAIL] {ip} - {msg}")
|
||||
fail += 1
|
||||
|
||||
logging.info(f"완료: 성공 {ok}대 / 실패 {fail}대")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
통합 GUID 수집 스크립트
|
||||
프로파일 기반으로 InfiniBand GUID 수집
|
||||
"""
|
||||
import sys
|
||||
import yaml
|
||||
import os
|
||||
from pathlib import Path
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import logging
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from core.idrac_client import IDRACClient
|
||||
from core.parsers import InfoParser
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s [INFO] root: %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
|
||||
def load_profile(profile_name: str) -> dict:
|
||||
"""프로파일 로드"""
|
||||
profile_file = Path(__file__).parent.parent / "profiles" / "guid_profiles.yaml"
|
||||
|
||||
if not profile_file.exists():
|
||||
logging.error(f"프로파일 파일 없음: {profile_file}")
|
||||
sys.exit(1)
|
||||
|
||||
with open(profile_file, 'r', encoding='utf-8') as f:
|
||||
profiles = yaml.safe_load(f)['profiles']
|
||||
|
||||
profile = profiles.get(profile_name, profiles['default'])
|
||||
|
||||
# 환경변수에서 슬롯 우선순위 읽기 (custom_priority 프로파일용)
|
||||
if not profile.get('slot_priority'):
|
||||
env_priority = os.getenv("GUID_SLOT_PRIORITY", "")
|
||||
if env_priority:
|
||||
profile['slot_priority'] = [int(s.strip()) for s in env_priority.split(",") if s.strip()]
|
||||
else:
|
||||
profile['slot_priority'] = profiles['default']['slot_priority']
|
||||
|
||||
return profile
|
||||
|
||||
|
||||
def collect_guid_for_ip(ip: str, profile: dict, output_dir: Path) -> tuple:
|
||||
"""단일 IP에 대한 GUID 정보 수집 (PortGUID_v1.py 로직 기반)"""
|
||||
try:
|
||||
logging.info(f"[{ip}] DEBUG: Running Unified GUID Script v2.2 (PortGUID_v1 Logic)")
|
||||
|
||||
# IDRAC credentials from env or default
|
||||
idrac_user = os.getenv("IDRAC_USER", "root")
|
||||
idrac_pass = os.getenv("IDRAC_PASS", "calvin")
|
||||
|
||||
# 1. Get Service Tag (using raw subprocess like v1)
|
||||
cmd_getsysinfo = [
|
||||
"racadm", "-r", ip, "-u", idrac_user, "-p", idrac_pass, "getsysinfo"
|
||||
]
|
||||
# v1 uses subprocess.getoutput which runs with shell=True/standard shell behavior.
|
||||
# Here we replicate it or use subprocess.run with capture_output.
|
||||
# PortGUID_v1 uses: getsysinfo = subprocess.getoutput(" ".join(cmd_getsysinfo))
|
||||
|
||||
# Using subprocess.run for better control but mimicking the command structure
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
try:
|
||||
# mimic v1: Use the exact same command string style
|
||||
proc_info = subprocess.run(cmd_getsysinfo, capture_output=True, text=True, timeout=60)
|
||||
getsysinfo = proc_info.stdout
|
||||
except Exception as e:
|
||||
return ip, False, f"SVC Tag 수집 실패: {e}"
|
||||
|
||||
svc_tag_match = re.search(r"SVC Tag\s*=\s*(\S+)", getsysinfo)
|
||||
svc_tag = svc_tag_match.group(1) if svc_tag_match else None
|
||||
|
||||
if not svc_tag:
|
||||
return ip, False, "서비스 태그 추출 실패 (Regex miss)"
|
||||
|
||||
# 2. Get InfiniBand Page List
|
||||
cmd_list = [
|
||||
"racadm", "-r", ip, "-u", idrac_user, "-p", idrac_pass, "get", "InfiniBand.VndrConfigPage"
|
||||
]
|
||||
proc_list = subprocess.run(cmd_list, capture_output=True, text=True, timeout=60)
|
||||
output_list = proc_list.stdout
|
||||
|
||||
# 3. Parse Page Map
|
||||
matches = re.findall(
|
||||
r"InfiniBand\.VndrConfigPage\.(\d+)\s+\[Key=InfiniBand\.Slot\.(\d+)-\d+#VndrConfigPage]",
|
||||
output_list
|
||||
)
|
||||
|
||||
slot_to_guid = {}
|
||||
# 4. Fetch Detail for each page
|
||||
for number, slot in matches:
|
||||
cmd_detail = [
|
||||
"racadm", "-r", ip, "-u", idrac_user, "-p", idrac_pass,
|
||||
"get", f"InfiniBand.VndrConfigPage.{number}"
|
||||
]
|
||||
proc_detail = subprocess.run(cmd_detail, capture_output=True, text=True, timeout=60)
|
||||
output_detail = proc_detail.stdout
|
||||
|
||||
match_guid = re.search(r"PortGUID=(\S+)", output_detail)
|
||||
port_guid = match_guid.group(1) if match_guid else None
|
||||
|
||||
if port_guid:
|
||||
slot_to_guid[int(slot)] = port_guid
|
||||
|
||||
# 5. Determine Priority (Preference: Profile > Env > Default(v1 logic))
|
||||
# Logic from PortGUID_v1.py:
|
||||
# if total_slots == 4: order = ...
|
||||
# elif total_slots == 10: order = ...
|
||||
|
||||
# We will use the profile's logic if available, but fallback to v1 logic if profile is 'default' and matches slot counts
|
||||
desired_order = profile.get('slot_priority', [])
|
||||
|
||||
# If profile didn't specify (or empty), use v1 adaptive logic
|
||||
if not desired_order:
|
||||
total_slots = len(matches)
|
||||
if total_slots == 4:
|
||||
desired_order = [38, 37, 32, 34]
|
||||
elif total_slots == 10:
|
||||
desired_order = [38, 39, 37, 36, 32, 33, 34, 35, 31, 40]
|
||||
else:
|
||||
desired_order = [int(slot) for _, slot in matches] # Just collection order
|
||||
logging.info(f"[{ip}] DEBUG: Using Adaptive Priority (Count={total_slots}): {desired_order}")
|
||||
else:
|
||||
logging.info(f"[{ip}] DEBUG: Using Profile Priority: {desired_order}")
|
||||
|
||||
# 6. Write File
|
||||
output_file = output_dir / f"{svc_tag}.txt"
|
||||
with output_file.open('w', encoding='utf-8', newline='\n') as f:
|
||||
f.write(f"{svc_tag}\n")
|
||||
|
||||
hex_guid_list = []
|
||||
|
||||
# Write in desired order
|
||||
for slot in desired_order:
|
||||
guid = slot_to_guid.get(slot)
|
||||
if guid:
|
||||
f.write(f"Slot.{slot}: {guid}\n")
|
||||
|
||||
# Format
|
||||
fmt_guid = guid.replace(':', '').upper()
|
||||
if not fmt_guid.startswith("0X"):
|
||||
fmt_guid = "0x" + fmt_guid
|
||||
hex_guid_list.append(fmt_guid)
|
||||
|
||||
# Remaining slots (if any weren't in priority list)?
|
||||
# v1 doesn't write them if not in priority list. We will stick to v1 behavior.
|
||||
|
||||
if profile.get('output_format', 'combined') == 'combined' and hex_guid_list:
|
||||
f.write(f"GUID: {';'.join(hex_guid_list)}\n")
|
||||
|
||||
return ip, True, f"저장: {output_file.name} ({len(hex_guid_list)}개 GUID/v1_Logic)"
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"[{ip}] 오류: {e}")
|
||||
return ip, False, f"오류: {e}"
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python collect_guid.py <ip_file> [profile_name]")
|
||||
print("Available profiles: default, custom_priority")
|
||||
sys.exit(1)
|
||||
|
||||
ip_file = Path(sys.argv[1])
|
||||
profile_name = sys.argv[2] if len(sys.argv) > 2 else "default"
|
||||
|
||||
if not ip_file.exists():
|
||||
logging.error(f"IP 파일 없음: {ip_file}")
|
||||
sys.exit(1)
|
||||
|
||||
# 프로파일 로드
|
||||
profile = load_profile(profile_name)
|
||||
logging.info(f"프로파일 사용: {profile_name} - {profile.get('description', '')}")
|
||||
logging.info(f"슬롯 우선순위: {profile['slot_priority']}")
|
||||
|
||||
# 출력 디렉토리
|
||||
# 출력 디렉토리 (스크립트 위치 기준)
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
data_root = script_dir.parent.parent
|
||||
output_dir = data_root / "temp" / "staging"
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# IP 목록 읽기
|
||||
ips = [line.strip() for line in ip_file.read_text(encoding='utf-8').splitlines() if line.strip()]
|
||||
|
||||
if not ips:
|
||||
logging.error("IP 목록이 비어 있습니다.")
|
||||
sys.exit(1)
|
||||
|
||||
logging.info(f"총 {len(ips)}대 처리 시작")
|
||||
|
||||
# 병렬 처리
|
||||
ok, fail = 0, 0
|
||||
with ThreadPoolExecutor(max_workers=20) as executor:
|
||||
futures = {executor.submit(collect_guid_for_ip, ip, profile, output_dir): ip for ip in ips}
|
||||
|
||||
for future in futures:
|
||||
ip, success, msg = future.result()
|
||||
if success:
|
||||
logging.info(f"[OK] {ip} - {msg}")
|
||||
ok += 1
|
||||
else:
|
||||
logging.error(f"[FAIL] {ip} - {msg}")
|
||||
fail += 1
|
||||
|
||||
logging.info(f"완료: 성공 {ok}대 / 실패 {fail}대")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
통합 MAC 수집 스크립트
|
||||
프로파일 기반으로 다양한 서버 타입 지원
|
||||
"""
|
||||
import sys
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import logging
|
||||
|
||||
# core 모듈 임포트
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from core.idrac_client import IDRACClient
|
||||
from core.parsers import InfoParser
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s [INFO] root: %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
|
||||
def load_profile(profile_name: str) -> dict:
|
||||
"""프로파일 로드 및 상속 처리"""
|
||||
profile_file = Path(__file__).parent.parent / "profiles" / "mac_profiles.yaml"
|
||||
|
||||
if not profile_file.exists():
|
||||
logging.error(f"프로파일 파일 없음: {profile_file}")
|
||||
sys.exit(1)
|
||||
|
||||
with open(profile_file, 'r', encoding='utf-8') as f:
|
||||
profiles = yaml.safe_load(f)['profiles']
|
||||
|
||||
profile = profiles.get(profile_name)
|
||||
if not profile:
|
||||
logging.error(f"프로파일 '{profile_name}' 없음. 사용 가능: {list(profiles.keys())}")
|
||||
sys.exit(1)
|
||||
|
||||
# extends 처리
|
||||
if 'extends' in profile:
|
||||
base = profiles[profile['extends']].copy()
|
||||
base.update(profile)
|
||||
profile = base
|
||||
|
||||
return profile
|
||||
|
||||
|
||||
def collect_mac_for_ip(ip: str, profile: dict, output_dir: Path) -> tuple:
|
||||
"""단일 IP에 대한 MAC 정보 수집"""
|
||||
try:
|
||||
client = IDRACClient(ip)
|
||||
parser = InfoParser()
|
||||
|
||||
# 데이터 수집
|
||||
sysinfo = client.get_sysinfo()
|
||||
hwinventory = client.get_hwinventory()
|
||||
|
||||
# 서비스 태그
|
||||
svc_tag = parser.extract_service_tag(sysinfo)
|
||||
if not svc_tag:
|
||||
return ip, False, "서비스 태그 추출 실패"
|
||||
|
||||
# NIC MAC 주소
|
||||
# NIC MAC 주소
|
||||
# sysinfo보다 hwinventory가 더 상세한 정보(Slot NIC 등)를 포함하므로 변경
|
||||
nic_macs = parser.extract_nic_macs_from_hwinventory(hwinventory, profile['nic_patterns'])
|
||||
|
||||
# iDRAC MAC
|
||||
idrac_mac = None
|
||||
if profile.get('include_idrac_mac'):
|
||||
idrac_mac = parser.extract_idrac_mac(sysinfo)
|
||||
|
||||
# InfiniBand (선택적)
|
||||
ib_guids = {}
|
||||
if 'infiniband_slots' in profile:
|
||||
# Legacy logic: Fetch from swinventory (checking previous line of FQDD)
|
||||
swinventory = client.get_swinventory()
|
||||
ib_guids = parser.extract_infiniband_macs_from_swinventory(swinventory, profile['infiniband_slots'])
|
||||
|
||||
# 벤더 정보
|
||||
memory_vendors = []
|
||||
disk_vendors = []
|
||||
|
||||
if profile.get('include_memory_vendor'):
|
||||
memory_vendors = parser.extract_vendors(hwinventory, "DIMM")
|
||||
|
||||
if profile.get('include_disk_vendor'):
|
||||
if profile.get('exclude_boss'):
|
||||
# BOSS 제외한 디스크 벤더
|
||||
all_vendors = set()
|
||||
for prefix in ["Disk.Bay.", "Disk.M.2.", "Disk.Direct.", "Disk.Slot."]:
|
||||
all_vendors.update(parser.extract_vendors(hwinventory, prefix))
|
||||
disk_vendors = sorted(all_vendors)
|
||||
else:
|
||||
disk_vendors = parser.extract_vendors(hwinventory, "Disk.")
|
||||
|
||||
# 파일 저장
|
||||
output_file = output_dir / f"{svc_tag}.txt"
|
||||
with output_file.open('w', encoding='utf-8', newline='\n') as f:
|
||||
f.write(f"{svc_tag}\n")
|
||||
|
||||
# NIC MAC (순서 유지)
|
||||
for pattern in profile['nic_patterns']:
|
||||
f.write(f"{nic_macs.get(pattern) or ''}\n")
|
||||
|
||||
# InfiniBand (있을 경우)
|
||||
if ib_guids:
|
||||
for slot in profile['infiniband_slots']:
|
||||
f.write(f"{ib_guids.get(slot) or ''}\n")
|
||||
|
||||
# iDRAC MAC
|
||||
if idrac_mac:
|
||||
f.write(f"{idrac_mac}\n")
|
||||
|
||||
# 벤더 정보
|
||||
for vendor in memory_vendors:
|
||||
f.write(f"{vendor}\n")
|
||||
for vendor in disk_vendors:
|
||||
f.write(f"{vendor}\n")
|
||||
|
||||
return ip, True, f"저장: {output_file.name}"
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"[{ip}] 오류: {e}")
|
||||
return ip, False, f"오류: {e}"
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python collect_mac.py <ip_file> [profile_name]")
|
||||
print("Available profiles: default, xe9680_h200_10ea, type11, jp_54ea, sp_18ea")
|
||||
sys.exit(1)
|
||||
|
||||
ip_file = Path(sys.argv[1])
|
||||
profile_name = sys.argv[2] if len(sys.argv) > 2 else "default"
|
||||
|
||||
if not ip_file.exists():
|
||||
logging.error(f"IP 파일 없음: {ip_file}")
|
||||
sys.exit(1)
|
||||
|
||||
# 프로파일 로드
|
||||
profile = load_profile(profile_name)
|
||||
logging.info(f"프로파일 사용: {profile_name} - {profile.get('description', '')}")
|
||||
|
||||
# 출력 디렉토리 (스크립트 위치 기준)
|
||||
# script: data/scripts/unified/collect_mac.py -> data_root: data/
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
data_root = script_dir.parent.parent
|
||||
output_dir = data_root / "temp" / "staging"
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# IP 목록 읽기
|
||||
ips = [line.strip() for line in ip_file.read_text(encoding='utf-8').splitlines() if line.strip()]
|
||||
|
||||
if not ips:
|
||||
logging.error("IP 목록이 비어 있습니다.")
|
||||
sys.exit(1)
|
||||
|
||||
logging.info(f"총 {len(ips)}대 처리 시작")
|
||||
|
||||
# 병렬 처리
|
||||
ok, fail = 0, 0
|
||||
with ThreadPoolExecutor(max_workers=20) as executor:
|
||||
futures = {executor.submit(collect_mac_for_ip, ip, profile, output_dir): ip for ip in ips}
|
||||
|
||||
for future in futures:
|
||||
ip, success, msg = future.result()
|
||||
if success:
|
||||
logging.info(f"[OK] {ip} - {msg}")
|
||||
ok += 1
|
||||
else:
|
||||
logging.error(f"[FAIL] {ip} - {msg}")
|
||||
fail += 1
|
||||
|
||||
logging.info(f"완료: 성공 {ok}대 / 실패 {fail}대")
|
||||
|
||||
# IP 파일 삭제 (기존 스크립트와 동일)
|
||||
try:
|
||||
ip_file.unlink(missing_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
통합 서버 정보 수집 스크립트
|
||||
프로파일 기반으로 BIOS/iDRAC/RAID 설정 수집
|
||||
"""
|
||||
import sys
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import logging
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from core.idrac_client import IDRACClient
|
||||
from core.parsers import InfoParser
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s [INFO] root: %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
|
||||
def load_profile(profile_name: str) -> dict:
|
||||
"""프로파일 로드 및 상속 처리"""
|
||||
profile_file = Path(__file__).parent.parent / "profiles" / "server_info_profiles.yaml"
|
||||
|
||||
if not profile_file.exists():
|
||||
logging.error(f"프로파일 파일 없음: {profile_file}")
|
||||
sys.exit(1)
|
||||
|
||||
with open(profile_file, 'r', encoding='utf-8') as f:
|
||||
profiles = yaml.safe_load(f)['profiles']
|
||||
|
||||
profile = profiles.get(profile_name)
|
||||
if not profile:
|
||||
logging.error(f"프로파일 '{profile_name}' 없음. 사용 가능: {list(profiles.keys())}")
|
||||
sys.exit(1)
|
||||
|
||||
# extends 처리
|
||||
if 'extends' in profile:
|
||||
base = profiles[profile['extends']].copy()
|
||||
# 리스트 병합
|
||||
for key in ['firmware', 'bios_settings', 'idrac_settings', 'raid_settings', 'custom_items']:
|
||||
if key in profile:
|
||||
base_items = base.get(key, [])
|
||||
profile_items = profile[key]
|
||||
# 중복 제거하면서 병합
|
||||
base[key] = base_items + profile_items
|
||||
base.update({k: v for k, v in profile.items() if k not in ['firmware', 'bios_settings', 'idrac_settings', 'raid_settings', 'custom_items']})
|
||||
profile = base
|
||||
|
||||
return profile
|
||||
|
||||
|
||||
def collect_server_info_for_ip(ip: str, profile: dict, output_dir: Path) -> tuple:
|
||||
"""단일 IP에 대한 서버 정보 수집"""
|
||||
try:
|
||||
client = IDRACClient(ip)
|
||||
parser = InfoParser()
|
||||
|
||||
# 서비스 태그 추출
|
||||
sysinfo = client.get_sysinfo()
|
||||
svc_tag = parser.extract_service_tag(sysinfo)
|
||||
if not svc_tag:
|
||||
return ip, False, "서비스 태그 추출 실패"
|
||||
|
||||
# 결과 파일 생성
|
||||
output_file = output_dir / f"{svc_tag}.txt"
|
||||
|
||||
with output_file.open('w', encoding='utf-8', newline='\n') as f:
|
||||
f.write(f"Dell EMC Server Bios,iDRAC,R/C Setting (SVC Tag: {svc_tag})\n\n")
|
||||
|
||||
# 펌웨어 버전 정보
|
||||
if 'firmware' in profile:
|
||||
f.write("-" * 42 + " Firmware Version 정보 " + "-" * 42 + "\n")
|
||||
for idx, item in enumerate(profile['firmware'], 1):
|
||||
value = _get_value_from_command(client, item['command'], item['pattern'])
|
||||
f.write(f"{idx}. {item['name']} : {value}\n")
|
||||
f.write("\n")
|
||||
|
||||
# BIOS 설정 정보
|
||||
if 'bios_settings' in profile:
|
||||
f.write("-" * 45 + " Bios 설정 정보 " + "-" * 46 + "\n")
|
||||
for idx, item in enumerate(profile['bios_settings'], 1):
|
||||
value = _get_value_from_command(client, item['command'], item['pattern'])
|
||||
f.write(f"{idx:02d}. {item['name']} : {value}\n")
|
||||
f.write("\n")
|
||||
|
||||
# iDRAC 설정 정보
|
||||
if 'idrac_settings' in profile:
|
||||
f.write("-" * 45 + " iDRAC 설정 정보 " + "-" * 45 + "\n")
|
||||
for idx, item in enumerate(profile['idrac_settings'], 1):
|
||||
value = _get_value_from_command(client, item['command'], item['pattern'])
|
||||
f.write(f"{idx:02d}. {item['name']} : {value}\n")
|
||||
f.write("\n")
|
||||
|
||||
# RAID 설정 정보
|
||||
if 'raid_settings' in profile:
|
||||
f.write("-" * 45 + " Raid 설정 정보 " + "-" * 46 + "\n")
|
||||
for idx, item in enumerate(profile['raid_settings'], 1):
|
||||
value = _get_value_from_command(client, item['command'], item['pattern'])
|
||||
f.write(f"{idx:02d}. {item['name']} : {value}\n")
|
||||
f.write("\n")
|
||||
|
||||
# 커스텀 항목 (있을 경우)
|
||||
if 'custom_items' in profile and profile['custom_items']:
|
||||
f.write("-" * 45 + " 커스텀 항목 " + "-" * 46 + "\n")
|
||||
for idx, item in enumerate(profile['custom_items'], 1):
|
||||
value = _get_value_from_command(client, item['command'], item['pattern'])
|
||||
f.write(f"{idx:02d}. {item['name']} : {value}\n")
|
||||
f.write("\n")
|
||||
|
||||
return ip, True, f"저장: {output_file.name}"
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"[{ip}] 오류: {e}")
|
||||
return ip, False, f"오류: {e}"
|
||||
|
||||
|
||||
def _get_value_from_command(client: IDRACClient, command: str, pattern: str) -> str:
|
||||
"""racadm 명령 실행 및 값 추출"""
|
||||
# 캐싱된 데이터 사용
|
||||
if command == "getsysinfo":
|
||||
output = client.get_sysinfo()
|
||||
elif command == "hwinventory":
|
||||
output = client.get_hwinventory()
|
||||
else:
|
||||
_, output = client.run_command(command)
|
||||
|
||||
# 패턴 매칭
|
||||
value = InfoParser.extract_value_by_pattern(output, pattern)
|
||||
return value if value else "N/A"
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python collect_server_info.py <ip_file> [profile_name]")
|
||||
print("Available profiles: default, intel_server, amd_server, minimal")
|
||||
sys.exit(1)
|
||||
|
||||
ip_file = Path(sys.argv[1])
|
||||
profile_name = sys.argv[2] if len(sys.argv) > 2 else "default"
|
||||
|
||||
if not ip_file.exists():
|
||||
logging.error(f"IP 파일 없음: {ip_file}")
|
||||
sys.exit(1)
|
||||
|
||||
# 프로파일 로드
|
||||
profile = load_profile(profile_name)
|
||||
logging.info(f"프로파일 사용: {profile_name} - {profile.get('description', '')}")
|
||||
|
||||
# 출력 디렉토리 (data/temp/staging)
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
# unified 디렉토리에 있으므로, data 디렉토리는 3단계 상위 (unified -> scripts -> data)
|
||||
data_dir = script_dir.parent.parent
|
||||
|
||||
# 만약 구조가 다를 경우를 대비한 안전장치 (scripts가 아닐 경우 등)
|
||||
if data_dir.name != "data":
|
||||
# fallback: 현재 작업 디렉토리 기준 data 찾기 시도
|
||||
data_dir = Path("data").resolve()
|
||||
|
||||
output_dir = data_dir / "temp" / "staging"
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# IP 목록 읽기
|
||||
ips = [line.strip() for line in ip_file.read_text(encoding='utf-8').splitlines() if line.strip()]
|
||||
|
||||
if not ips:
|
||||
logging.error("IP 목록이 비어 있습니다.")
|
||||
sys.exit(1)
|
||||
|
||||
logging.info(f"총 {len(ips)}대 처리 시작")
|
||||
|
||||
# 병렬 처리 (서버 정보 수집은 racadm 호출이 많아 worker 수 줄임)
|
||||
ok, fail = 0, 0
|
||||
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||
futures = {executor.submit(collect_server_info_for_ip, ip, profile, output_dir): ip for ip in ips}
|
||||
|
||||
for future in futures:
|
||||
ip, success, msg = future.result()
|
||||
if success:
|
||||
logging.info(f"[OK] {ip} - {msg}")
|
||||
ok += 1
|
||||
else:
|
||||
logging.error(f"[FAIL] {ip} - {msg}")
|
||||
fail += 1
|
||||
|
||||
logging.info(f"완료: 성공 {ok}대 / 실패 {fail}대")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
통합 시스템 제어 스크립트 (Unified System Control Script)
|
||||
기존의 개별 제어 스크립트들을 하나로 통합하였습니다.
|
||||
|
||||
지원 기능:
|
||||
- power_on (06-PowerON.py)
|
||||
- power_off (07-PowerOFF.py)
|
||||
- log_clear (05-clrsel.py)
|
||||
- job_delete (08-job_delete_all.py)
|
||||
- tsr_collect (03-tsr_log.py)
|
||||
- tsr_save (04-tsr_save.py)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import subprocess
|
||||
import time
|
||||
import logging
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s [INFO] %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Load .env relative to script location or current directory
|
||||
load_dotenv()
|
||||
|
||||
IDRAC_USER = os.getenv("IDRAC_USER", "root")
|
||||
IDRAC_PASS = os.getenv("IDRAC_PASS", "calvin")
|
||||
OME_USER = os.getenv("OME_USER", "")
|
||||
OME_PASS = os.getenv("OME_PASS", "")
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Command Definitions
|
||||
# -----------------------------------------------------------------------------
|
||||
# Each action maps to a function that returns the racadm command list
|
||||
def cmd_power_on(ip):
|
||||
return ["racadm", "-r", ip, "-u", IDRAC_USER, "-p", IDRAC_PASS, "serveraction", "powerup"]
|
||||
|
||||
def cmd_power_off(ip):
|
||||
return ["racadm", "-r", ip, "-u", IDRAC_USER, "-p", IDRAC_PASS, "serveraction", "powerdown"]
|
||||
|
||||
def cmd_log_clear(ip):
|
||||
return ["racadm", "-r", ip, "-u", IDRAC_USER, "-p", IDRAC_PASS, "clrsel"]
|
||||
|
||||
def cmd_job_delete(ip):
|
||||
return ["racadm", "-r", ip, "-u", IDRAC_USER, "-p", IDRAC_PASS, "jobqueue", "delete", "-i", "ALL"]
|
||||
|
||||
def cmd_tsr_collect(ip):
|
||||
return ["racadm", "-r", ip, "-u", IDRAC_USER, "-p", IDRAC_PASS, "techsupreport", "collect"]
|
||||
|
||||
def cmd_tsr_save(ip):
|
||||
# Note: This command assumes a specific share path (legacy behavior)
|
||||
# Ideally this path should also be configurable via env
|
||||
share_path = "//10.10.3.15/share/"
|
||||
return [
|
||||
"racadm", "-r", ip, "-u", IDRAC_USER, "-p", IDRAC_PASS,
|
||||
"techsupreport", "export", "-l", share_path, "-u", OME_USER, "-p", OME_PASS
|
||||
]
|
||||
|
||||
ACTION_MAP = {
|
||||
"power_on": cmd_power_on,
|
||||
"power_off": cmd_power_off,
|
||||
"log_clear": cmd_log_clear,
|
||||
"job_delete": cmd_job_delete,
|
||||
"tsr_collect": cmd_tsr_collect,
|
||||
"tsr_save": cmd_tsr_save,
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Execution Logic
|
||||
# -----------------------------------------------------------------------------
|
||||
def execute_action(ip: str, action_name: str) -> None:
|
||||
"""Run the racadm command for a single IP."""
|
||||
try:
|
||||
cmd_func = ACTION_MAP.get(action_name)
|
||||
if not cmd_func:
|
||||
logger.error(f"[{ip}] Unknown action: {action_name}")
|
||||
return
|
||||
|
||||
cmd = cmd_func(ip)
|
||||
logger.info(f"[{ip}] Executing {action_name}...")
|
||||
|
||||
# Run command
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600 # Generous timeout for TSR operations
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
logger.info(f"[{ip}] Success: {result.stdout.strip()}")
|
||||
else:
|
||||
logger.error(f"[{ip}] Failed: {result.stderr.strip()}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{ip}] Exception: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Unified System Control Script")
|
||||
parser.add_argument("action", choices=ACTION_MAP.keys(), help="Action to perform")
|
||||
parser.add_argument("ip_file", type=Path, help="Path to the IP list file")
|
||||
|
||||
# Optional: Allow passing IP directly via args instead of file (future extension)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
ip_file = args.ip_file
|
||||
action = args.action
|
||||
|
||||
if not ip_file.is_file():
|
||||
logger.error(f"IP file not found: {ip_file}")
|
||||
sys.exit(1)
|
||||
|
||||
# Read IPs
|
||||
try:
|
||||
with ip_file.open("r", encoding="utf-8") as f:
|
||||
ips = [line.strip() for line in f if line.strip()]
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to read IP file: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if not ips:
|
||||
logger.warning("No IPs found in file.")
|
||||
sys.exit(0)
|
||||
|
||||
logger.info(f"Starting '{action}' for {len(ips)} servers...")
|
||||
start_time = time.time()
|
||||
|
||||
# Parallel Execution
|
||||
# Adjust max_workers as needed. 20 is a safe defaults for I/O bound network tasks.
|
||||
with ThreadPoolExecutor(max_workers=20) as executor:
|
||||
futures = [executor.submit(execute_action, ip, action) for ip in ips]
|
||||
for future in futures:
|
||||
future.result() # Wait for all to complete
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
logger.info(f"Completed '{action}' in {elapsed:.2f} seconds.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user