- itsm/ -> workspace/guardia-itsm/ - manager/ -> workspace/guardia-manager/ - app/ -> workspace/guardia-messenger/ - manual/ -> workspace/guardia-docs/ workspace/zioinfo-web/ unchanged. git mv preserves full commit history. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
258 lines
11 KiB
Python
258 lines
11 KiB
Python
"""
|
|
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}
|