update
This commit is contained in:
@@ -7,7 +7,7 @@ from .main import register_main_routes
|
||||
from .xml import register_xml_routes
|
||||
from .utilities import register_util_routes
|
||||
from .file_view import register_file_view
|
||||
|
||||
from .jobs import register_jobs_routes
|
||||
|
||||
def register_routes(app: Flask, socketio=None) -> None:
|
||||
"""블루프린트 일괄 등록. socketio는 main 라우트에서만 사용."""
|
||||
@@ -17,4 +17,5 @@ def register_routes(app: Flask, socketio=None) -> None:
|
||||
register_main_routes(app, socketio)
|
||||
register_xml_routes(app)
|
||||
register_util_routes(app)
|
||||
register_file_view(app)
|
||||
register_file_view(app)
|
||||
register_jobs_routes(app)
|
||||
279
backend/routes/jobs.py
Normal file
279
backend/routes/jobs.py
Normal file
@@ -0,0 +1,279 @@
|
||||
"""
|
||||
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")
|
||||
@@ -299,4 +299,4 @@ def download_excel():
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
logging.info(f"엑셀 파일 다운로드: {path}")
|
||||
return send_file(str(path), as_attachment=True, download_name="mac_info.xlsx")
|
||||
return send_file(str(path), as_attachment=True, download_name="mac_info.xlsx")
|
||||
Reference in New Issue
Block a user