""" Work execution router — work-log CRUD, SSH simulation, completion + messenger notify. """ import asyncio import json from datetime import datetime from typing import List, Optional try: import httpx _HTTPX = True except ImportError: _HTTPX = False from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user from database import get_db from models import ( AuditLog, Institution, Rating, Server, SRRequest, SRStatus, User, WorkActionType, WorkLog, WorkLogOut, WorkStepIn, compute_log_hash, ) router = APIRouter(prefix="/api/work", tags=["work"]) # ── Messenger webhook URL (same machine) ────────────────────────────────────── MESSENGER_WEBHOOK = "http://localhost:8000/api/messenger/webhook" NOTIFY_ROOM = "ops" # 알림 채널 # ── SSH 시뮬레이션 템플릿 ───────────────────────────────────────────────────── _SSH_SIM: dict[str, list[dict]] = { "RESTART": [ {"action": WorkActionType.SSH_CONNECT, "content": "SSH 접속 시도 (opsagent@{server}:22)", "result": "Connected to {server} — OpenSSH_8.7, RHEL 8.9"}, {"action": WorkActionType.SSH_EXEC, "content": "systemctl stop tomcat9 && sleep 2 && systemctl start tomcat9", "result": "● tomcat9.service: active (running) since {ts}"}, {"action": WorkActionType.HEALTH_CHECK, "content": "curl -sf http://localhost:8080/health -o /dev/null -w '%{http_code}'", "result": "200 OK — 응답 시간 42ms"}, ], "DEPLOY": [ {"action": WorkActionType.SSH_CONNECT, "content": "SFTP 접속 시도 (opsagent@{server}:22)", "result": "sftp> Connected to {server}"}, {"action": WorkActionType.SOURCE_MOD, "content": "파일 전송: put -r ./classes /app/was/webapps/ROOT/WEB-INF/classes/", "result": "전송 완료 — 23 files, 1.4 MB (0.8s)"}, {"action": WorkActionType.SSH_EXEC, "content": "systemctl reload tomcat9", "result": "Reload OK — 클래스 파일 적용됨"}, {"action": WorkActionType.HEALTH_CHECK, "content": "curl -sf http://localhost:8080/actuator/health", "result": '{"status":"UP","components":{"db":{"status":"UP"}}}'}, ], "LOG": [ {"action": WorkActionType.SSH_CONNECT, "content": "SSH 접속 ({server}:22) — 로그 분석 모드", "result": "Connected"}, {"action": WorkActionType.SSH_EXEC, "content": "tail -n 200 /app/was/logs/catalina.out | grep -E 'ERROR|WARN'", "result": ( "[ERROR] 2026-05-24 17:32:11 ORA-01555: snapshot too old\n" "[WARN] 2026-05-24 17:33:05 Connection pool exhausted (200/200)\n" "[ERROR] 2026-05-24 17:34:22 java.lang.OutOfMemoryError: GC overhead" )}, ], "DEFAULT": [ {"action": WorkActionType.SSH_CONNECT, "content": "SSH 접속 ({server}:22)", "result": "Connected"}, {"action": WorkActionType.SSH_EXEC, "content": "작업 수행 중…", "result": "명령 실행 완료"}, ], } async def _write_audit(db: AsyncSession, sr_id: str, actor: str, action: str, detail: str) -> None: result = await db.execute( select(AuditLog).where(AuditLog.sr_id == sr_id) .order_by(AuditLog.id.desc()).limit(1) ) last = result.scalars().first() prev_hash = last.log_hash if last else None ts = datetime.now().isoformat() log_hash = compute_log_hash(prev_hash, actor, action, detail, ts) db.add(AuditLog(sr_id=sr_id, actor=actor, action=action, detail=detail, prev_hash=prev_hash, log_hash=log_hash)) async def _notify_messenger(sr: SRRequest, work_summary: str) -> None: """완료 시 메신저 webhook 호출.""" if not _HTTPX: return payload = { "event": "itsm_complete", "room": NOTIFY_ROOM, "sr_id": sr.sr_id, "title": sr.title, "sr_type": sr.sr_type, "requested_by": sr.requested_by, "target_server": sr.target_server or "—", "result_summary": work_summary, } try: async with httpx.AsyncClient(timeout=5.0) as client: await client.post(MESSENGER_WEBHOOK, json=payload) except Exception: pass # 메신저 미동작 시 무시 # ── Endpoints ───────────────────────────────────────────────────────────────── @router.get("/{sr_id}", response_model=List[WorkLogOut]) async def list_work_logs(sr_id: str, db: AsyncSession = Depends(get_db), _u: User = Depends(get_current_user)): result = await db.execute( select(WorkLog).where(WorkLog.sr_id == sr_id).order_by(WorkLog.created_at) ) return result.scalars().all() @router.post("/{sr_id}/step", response_model=WorkLogOut, status_code=201) async def add_work_step(sr_id: str, payload: WorkStepIn, db: AsyncSession = Depends(get_db), _u: User = Depends(get_current_user)): r = await db.execute(select(SRRequest).where(SRRequest.sr_id == sr_id)) if not r.scalars().first(): raise HTTPException(404, "SR을 찾을 수 없습니다.") log = WorkLog(sr_id=sr_id, **payload.model_dump()) db.add(log) await db.commit() await db.refresh(log) return log @router.post("/{sr_id}/simulate") async def simulate_work(sr_id: str, engineer: str = "GUARDiA-AI", db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)): """ 전체 작업 흐름 시뮬레이션: CMDB 확인 → SSH 접속 → 작업 실행 → 헬스체크 → SR COMPLETED → 메신저 알림 """ r = await db.execute(select(SRRequest).where(SRRequest.sr_id == sr_id)) sr = r.scalars().first() if not sr: raise HTTPException(404, "SR을 찾을 수 없습니다.") ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") server = sr.target_server or "UNKNOWN-SRV" # ── Step 1: CMDB 자산 확인 ────────────────────────────────────────────── srv_info = "" if sr.target_server: sv = await db.execute( select(Server).where(Server.server_name == sr.target_server) ) sv_obj = sv.scalars().first() if sv_obj: srv_info = f"{sv_obj.server_role} | {sv_obj.os_type} {sv_obj.os_version} | SSH:22" cmdb_log = WorkLog( sr_id=sr_id, engineer=engineer, action_type=WorkActionType.CMDB_CHECK, content=f"CMDB 자산 조회: {server}", result=srv_info or f"{server} — CMDB 등록 서버 확인됨", is_success=True, ) db.add(cmdb_log) # ── SR 상태 자동 전이: RECEIVED/PARSED → PENDING_APPROVAL → APPROVED ──── auto_approve = sr.status in ( SRStatus.RECEIVED, SRStatus.PARSED, SRStatus.PENDING_APPROVAL, SRStatus.APPROVED ) if sr.status in (SRStatus.RECEIVED, SRStatus.PARSED): sr.status = SRStatus.PENDING_APPROVAL await _write_audit(db, sr_id, engineer, "AUTO_PENDING", "시뮬레이션: 승인 대기 전이") if sr.status == SRStatus.PENDING_APPROVAL: sr.status = SRStatus.APPROVED await _write_audit(db, sr_id, "AUTO_APPROVE", "SR_APPROVED", "시뮬레이션: 자동 승인") # IN_PROGRESS 전이 sr.status = SRStatus.IN_PROGRESS sr.updated_at = datetime.now() await _write_audit(db, sr_id, engineer, "SR_STARTED", "작업 시작") await db.flush() # ── Step 2~N: SR type별 SSH 시뮬레이션 ─────────────────────────────────── steps = _SSH_SIM.get(sr.sr_type, _SSH_SIM["DEFAULT"]) result_summary = "" for step in steps: wlog = WorkLog( sr_id=sr_id, engineer=engineer, action_type=step["action"], content=step["content"].format(server=server, ts=ts), result=step["result"].format(server=server, ts=ts), is_success=True, ) db.add(wlog) result_summary = wlog.result # ── Step Final: RESULT 기록 ─────────────────────────────────────────────── final_msg = f"{sr.title} 처리 완료 — {result_summary[:80]}" db.add(WorkLog( sr_id=sr_id, engineer=engineer, action_type=WorkActionType.RESULT, content="작업 결과 기록", result=final_msg, is_success=True, )) # ── SR → COMPLETED ──────────────────────────────────────────────────────── sr.status = SRStatus.COMPLETED sr.updated_at = datetime.now() await _write_audit(db, sr_id, engineer, "SR_COMPLETED", final_msg) await db.commit() # ── 메신저 + 이메일 알림 (비동기, 실패 무시) ───────────────────────────── asyncio.create_task(_notify_messenger(sr, final_msg)) from core.notify import notify_sr_status_changed as _notify asyncio.create_task(_notify(sr, SRStatus.COMPLETED, final_msg)) return {"status": "COMPLETED", "sr_id": sr_id, "summary": final_msg} @router.post("/{sr_id}/complete") async def manual_complete(sr_id: str, engineer: str = "엔지니어", result_note: str = "수동 완료 처리", db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)): """수동 완료 처리 + 메신저 알림.""" r = await db.execute(select(SRRequest).where(SRRequest.sr_id == sr_id)) sr = r.scalars().first() if not sr: raise HTTPException(404, "SR을 찾을 수 없습니다.") sr.status = SRStatus.COMPLETED sr.updated_at = datetime.now() db.add(WorkLog( sr_id=sr_id, engineer=engineer, action_type=WorkActionType.COMPLETE, content="수동 완료 처리", result=result_note, is_success=True, )) await _write_audit(db, sr_id, engineer, "SR_COMPLETED", result_note) await db.commit() asyncio.create_task(_notify_messenger(sr, result_note)) from core.notify import notify_sr_status_changed as _notify asyncio.create_task(_notify(sr, SRStatus.COMPLETED, result_note)) return {"status": "COMPLETED", "sr_id": sr_id}