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
+123
View File
@@ -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()
+216
View File
@@ -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()
+191
View File
@@ -0,0 +1,191 @@
#!/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 = []
boss_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 제외)
regular_vendors = set()
for prefix in ["Disk.Bay.", "Disk.M.2.", "Disk.Slot."]:
regular_vendors.update(parser.extract_vendors(hwinventory, prefix))
disk_vendors = sorted(regular_vendors)
# BOSS 디스크 벤더 별도 수집 (Disk.Direct. 중 BOSS 포함 항목만)
boss_disk_vendors = parser.extract_vendors(hwinventory, "Disk.Direct.", instance_must_contain="BOSS")
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")
for vendor in boss_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()
+192
View File
@@ -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()
+239
View File
@@ -0,0 +1,239 @@
#!/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
import re
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import Optional
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")
TSR_SHARE_URL = os.getenv("TSR_SHARE_URL", "//10.10.3.15/share/")
OME_USER = os.getenv("OME_USER", "")
OME_PASS = os.getenv("OME_PASS", "")
TSR_JOB_TIMEOUT = int(os.getenv("TSR_JOB_TIMEOUT", "900"))
TSR_JOB_POLL_INTERVAL = int(os.getenv("TSR_JOB_POLL_INTERVAL", "10"))
TSR_SAVE_WORKERS = int(os.getenv("TSR_SAVE_WORKERS", "30"))
# -----------------------------------------------------------------------------
# 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):
return [
"racadm", "-r", ip, "-u", IDRAC_USER, "-p", IDRAC_PASS,
"techsupreport", "export", "-l", TSR_SHARE_URL, "-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 redact_cmd(cmd):
redacted = []
hide_next = False
for part in cmd:
if hide_next:
redacted.append("***")
hide_next = False
continue
redacted.append(part)
if part == "-p":
hide_next = True
return redacted
def extract_job_id(output: str) -> Optional[str]:
match = re.search(r"Job ID\s*=\s*(JID_\d+)", output or "")
return match.group(1) if match else None
def wait_for_job(ip: str, job_id: str) -> bool:
deadline = time.time() + TSR_JOB_TIMEOUT
view_cmd = [
"racadm", "-r", ip, "-u", IDRAC_USER, "-p", IDRAC_PASS,
"jobqueue", "view", "-i", job_id
]
while time.time() < deadline:
result = subprocess.run(
view_cmd,
capture_output=True,
text=True,
timeout=120
)
output = "\n".join(part for part in [result.stdout.strip(), result.stderr.strip()] if part)
logger.info(f"[{ip}] Job {job_id} status:\n{output}")
if "Status=Completed" in output:
return True
if "Status=Failed" in output:
return False
time.sleep(TSR_JOB_POLL_INTERVAL)
logger.error(f"[{ip}] Job {job_id} status check timed out after {TSR_JOB_TIMEOUT} seconds.")
return False
def execute_action(ip: str, action_name: str) -> bool:
"""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 False
cmd = cmd_func(ip)
logger.info(f"[{ip}] Executing {action_name}...")
if action_name == "tsr_save":
logger.info(f"[{ip}] Command: {redact_cmd(cmd)}")
# Run command
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=600 # Generous timeout for TSR operations
)
output = "\n".join(part for part in [result.stdout.strip(), result.stderr.strip()] if part)
if result.returncode == 0:
logger.info(f"[{ip}] Success: {output}")
if action_name == "tsr_save":
job_id = extract_job_id(output)
if not job_id:
logger.error(f"[{ip}] TSR Save did not return a Job ID.")
return False
return wait_for_job(ip, job_id)
return True
else:
logger.error(f"[{ip}] Failed: {output}")
return False
except Exception as e:
logger.error(f"[{ip}] Exception: {e}")
return False
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()
success_count = 0
failed_count = 0
if action == "tsr_save":
max_workers = max(1, min(TSR_SAVE_WORKERS, len(ips)))
logger.info(f"TSR Save parallel workers={max_workers}")
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [executor.submit(execute_action, ip, action) for ip in ips]
for future in futures:
if future.result():
success_count += 1
else:
failed_count += 1
else:
# 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:
if future.result():
success_count += 1
else:
failed_count += 1
elapsed = time.time() - start_time
logger.info(
f"Completed '{action}' in {elapsed:.2f} seconds. "
f"success={success_count}, failed={failed_count}"
)
if failed_count:
sys.exit(1)
if __name__ == "__main__":
main()