152 lines
6.2 KiB
Python
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 |