Files
iDRAC_Info/backend/services/ip_processor.py
2025-10-05 17:37:51 +09:00

152 lines
6.2 KiB
Python

from __future__ import annotations
from pathlib import Path
import os
import sys
import uuid
import logging
import subprocess
import platform
from concurrent.futures import ThreadPoolExecutor, as_completed
from threading import Lock
from watchdog.observers import Observer
from backend.services.watchdog_handler import FileCreatedHandler
from config import Config
# Job ID별 진행률 (스레드 안전)
_progress: dict[str, int] = {}
_progress_lock = Lock()
def _set_progress(job_id: str, value: int) -> None:
with _progress_lock:
_progress[job_id] = max(0, min(100, int(value)))
def get_progress(job_id: str) -> int:
with _progress_lock:
return int(_progress.get(job_id, 0))
def on_complete(job_id: str) -> None:
_set_progress(job_id, 100)
# ─────────────────────────────────────────────────────────────
# IP 목록 저장
# ─────────────────────────────────────────────────────────────
def save_ip_addresses(ips: str, folder: str | os.PathLike[str]) -> list[tuple[str, str]]:
out_dir = Path(folder)
out_dir.mkdir(parents=True, exist_ok=True)
ip_files: list[tuple[str, str]] = []
for i, raw in enumerate((ips or "").splitlines()):
ip = raw.strip()
if not ip:
continue
file_path = out_dir / f"ip_{i}.txt"
file_path.write_text(ip + "\n", encoding="utf-8")
ip_files.append((ip, str(file_path)))
return ip_files
# ─────────────────────────────────────────────────────────────
# 개별 IP 처리
# ─────────────────────────────────────────────────────────────
def _build_command(script: str, ip_file: str, xml_file: str | None) -> list[str]:
script_path = Path(Config.SCRIPT_FOLDER) / script
if not script_path.exists():
raise FileNotFoundError(f"스크립트를 찾을 수 없습니다: {script_path}")
if script_path.suffix == ".sh":
# Windows에서 .sh 실행은 bash 필요 (Git Bash/WSL 등). 없으면 예외 처리.
if platform.system() == "Windows":
bash = shutil.which("bash") # type: ignore[name-defined]
if not bash:
raise RuntimeError("Windows에서 .sh 스크립트를 실행하려면 bash가 필요합니다.")
cmd = [bash, str(script_path), ip_file]
else:
cmd = [str(script_path), ip_file]
elif script_path.suffix == ".py":
cmd = [sys.executable, str(script_path), ip_file]
else:
raise ValueError(f"지원되지 않는 스크립트 형식: {script_path.suffix}")
if xml_file:
cmd.append(xml_file)
return cmd
def process_ip(ip_file: str, script: str, xml_file: str | None = None) -> None:
ip = Path(ip_file).read_text(encoding="utf-8").strip()
cmd = _build_command(script, ip_file, xml_file)
logging.info("🔧 실행 명령: %s", " ".join(cmd))
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True,
cwd=str(Path(Config.SCRIPT_FOLDER)),
timeout=int(os.getenv("SCRIPT_TIMEOUT", "1800")), # 30분 기본
)
logging.info("[%s] ✅ stdout:\n%s", ip, result.stdout)
if result.stderr:
logging.warning("[%s] ⚠ stderr:\n%s", ip, result.stderr)
except subprocess.CalledProcessError as e:
logging.error("[%s] ❌ 스크립트 실행 오류(code=%s): %s", ip, e.returncode, e.stderr or e)
except subprocess.TimeoutExpired:
logging.error("[%s] ⏰ 스크립트 실행 타임아웃", ip)
# ─────────────────────────────────────────────────────────────
# 병렬 처리 진입점
# ─────────────────────────────────────────────────────────────
def process_ips_concurrently(ip_files, job_id, observer: Observer, script: str, xml_file: str | 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}
for fut in as_completed(futures):
ip = futures[fut]
try:
fut.result()
except Exception as e:
logging.error("%s 처리 중 오류 발생: %s", ip, e)
finally:
completed += 1
if total:
_set_progress(job_id, int(completed * 100 / total))
finally:
try:
observer.stop()
observer.join(timeout=5)
except Exception:
pass
# ─────────────────────────────────────────────────────────────
# 외부에서 한 번에 처리(동기)
# ─────────────────────────────────────────────────────────────
def handle_ip_processing(ip_text: str, script: str, xml_file: str | None = None) -> str:
job_id = str(uuid.uuid4())
temp_dir = Path(Config.UPLOAD_FOLDER) / job_id
ip_files = save_ip_addresses(ip_text, temp_dir)
xml_path = str(Path(Config.XML_FOLDER) / xml_file) if xml_file else None
handler = FileCreatedHandler(job_id, len(ip_files))
observer = Observer()
observer.schedule(handler, Config.IDRAC_INFO_FOLDER, recursive=False)
observer.start()
process_ips_concurrently(ip_files, job_id, observer, script, xml_path)
return job_id