""" Flask Blueprint for iDRAC Job Monitoring (Redfish 버전) 기존 routes/jobs.py 또는 backend/routes/jobs.py를 이 파일로 교체하세요. """ import time import logging from flask import Blueprint, render_template, jsonify, request from flask_login import login_required from backend.services.idrac_jobs import ( scan_all, parse_ip_list, load_ip_list, LRUJobCache, is_active_status, is_done_status, parse_iso_datetime, iso_now ) import os logger = logging.getLogger(__name__) # Blueprint 생성 jobs_bp = Blueprint("jobs", __name__, url_prefix="/jobs") # Job 캐시 (전역) MAX_CACHE_SIZE = int(os.getenv("MAX_CACHE_SIZE", "10000")) CACHE_GC_INTERVAL = int(os.getenv("CACHE_GC_INTERVAL", "3600")) JOB_GRACE_MINUTES = int(os.getenv("JOB_GRACE_MINUTES", "60")) JOB_RECENCY_HOURS = int(os.getenv("JOB_RECENCY_HOURS", "24")) JOB_CACHE = LRUJobCache(max_size=MAX_CACHE_SIZE) # ──────────────────────────────────────────────────────────── # Routes # ──────────────────────────────────────────────────────────── @jobs_bp.route("", methods=["GET"]) @login_required def jobs_page(): """메인 페이지""" return render_template("jobs.html") @jobs_bp.route("/config", methods=["GET"]) @login_required def jobs_config(): """프론트엔드 설정 제공""" return jsonify({ "ok": True, "config": { "grace_minutes": JOB_GRACE_MINUTES, "recency_hours": JOB_RECENCY_HOURS, "poll_interval_ms": int(os.getenv("POLL_INTERVAL_MS", "10000")), } }) @jobs_bp.route("/iplist", methods=["GET"]) @login_required def get_ip_list(): """IP 목록 조회 (파일에서)""" try: ips = load_ip_list() return jsonify({ "ok": True, "ips": ips, "count": len(ips) }) except Exception as e: logger.exception("Failed to load IP list") return jsonify({ "ok": False, "error": str(e) }), 500 @jobs_bp.route("/scan", methods=["POST"]) @login_required def scan_jobs(): """ Job 스캔 및 모니터링 Request Body: { "ips": List[str] (optional), "method": "redfish" (기본값), "recency_hours": int (기본: 24), "grace_minutes": int (기본: 60), "include_tracked_done": bool (기본: True) } Response: { "ok": True, "count": int, "items": [ { "ip": str, "ok": bool, "error": str (if not ok), "jobs": List[Dict] } ] } """ data = request.get_json(silent=True) or {} # IP 목록 ip_input = data.get("ips") if ip_input: ips = parse_ip_list("\n".join(ip_input) if isinstance(ip_input, list) else str(ip_input)) else: ips = load_ip_list() if not ips: return jsonify({ "ok": False, "error": "No IPs provided", "items": [] }), 400 # 파라미터 method = data.get("method", "redfish") # redfish가 기본값 recency_hours = int(data.get("recency_hours", JOB_RECENCY_HOURS)) grace_minutes = int(data.get("grace_minutes", JOB_GRACE_MINUTES)) include_tracked_done = bool(data.get("include_tracked_done", True)) grace_sec = grace_minutes * 60 cutoff = time.time() - recency_hours * 3600 # 현재 IP 목록과 다른 캐시 항목 제거 JOB_CACHE.clear_for_ips(set(ips)) # 스캔 실행 try: items = scan_all(ips, method=method) except Exception as e: logger.exception("Scan failed") return jsonify({ "ok": False, "error": str(e), "items": [] }), 500 now = time.time() # 캐시 업데이트 for item in items: ip = item.get("ip", "") if not item.get("ok") or not isinstance(item.get("jobs"), list): continue for job in item["jobs"]: status = job.get("Status") message = job.get("Message") active_now = is_active_status(status, message) done_now = is_done_status(status) # 시작 시간 파싱 start_ts = parse_iso_datetime(job.get("StartTime")) # 리센시 판정 if not active_now: if start_ts is None or start_ts < cutoff: continue # 캐시 키 생성 key = _make_cache_key(ip, job) entry = JOB_CACHE.get(key) if entry is None: JOB_CACHE.set(key, { "record": dict(job), "first_seen_active": (now if active_now else None), "became_done_at": (now if done_now else None), "first_seen": now, "last_seen": now, "start_ts": start_ts, }) else: entry["record"] = dict(job) entry["last_seen"] = now if active_now and not entry.get("first_seen_active"): entry["first_seen_active"] = now if done_now and not entry.get("became_done_at"): entry["became_done_at"] = now elif not done_now: entry["became_done_at"] = None if start_ts: entry["start_ts"] = start_ts JOB_CACHE.set(key, entry) # 응답 생성 out_items = [] for item in items: ip = item.get("ip", "") shown_jobs = [] # 현재 Active Job current_active = [] if item.get("ok") and isinstance(item.get("jobs"), list): for job in item["jobs"]: if is_active_status(job.get("Status"), job.get("Message")): key = _make_cache_key(ip, job) if key in JOB_CACHE.keys(): current_active.append(JOB_CACHE.get(key)["record"]) if current_active: shown_jobs = current_active else: # Active가 없을 때: 추적된 최근 완료 Job 표시 if include_tracked_done: for key in JOB_CACHE.keys(): if key[0] != ip: continue entry = JOB_CACHE.get(key) if not entry: continue start_ok = (entry.get("start_ts") or 0) >= cutoff done_at = entry.get("became_done_at") done_ok = bool(done_at and now - done_at <= grace_sec) still_active = entry.get("became_done_at") is None if still_active and start_ok: shown_jobs.append(entry["record"]) elif done_ok and start_ok: rec = dict(entry["record"]) rec["RecentlyCompleted"] = True rec["CompletedAt"] = iso_now() shown_jobs.append(rec) out_items.append({ "ip": ip, "ok": item.get("ok"), "error": item.get("error"), "jobs": sorted(shown_jobs, key=lambda r: r.get("JID", "")) }) # 캐시 GC (조건부) if now - JOB_CACHE.last_gc >= CACHE_GC_INTERVAL: JOB_CACHE.gc(max_age_seconds=24 * 3600) return jsonify({ "ok": True, "count": len(out_items), "items": out_items }) def _make_cache_key(ip: str, job: dict): """캐시 키 생성""" jid = (job.get("JID") or "").strip() if jid: return (ip, jid) name = (job.get("Name") or "").strip() return (ip, f"NOJID::{name}") # ──────────────────────────────────────────────────────────── # 기존 패턴에 맞는 register 함수 추가 # ──────────────────────────────────────────────────────────── def register_jobs_routes(app): """ iDRAC Job 모니터링 라우트 등록 기존 프로젝트 패턴에 맞춘 함수 """ from flask import Flask app.register_blueprint(jobs_bp) logger.info("Jobs routes registered at /jobs")