zioinfo-mail/workspace/guardia-itsm/routers/servicenow.py
DESKTOP-TKLFCPR\ython fc0ba65e05 feat(expansion): GUARDiA v3 P3 완성 — 13 routers + 14 DB tables
라우터 (667개 엔드포인트, P3 신규 69개):
- multimodal.py:      llava 이미지 분석 + 에러 자동 분류
- learning_loop.py:   Ollama 파인튜닝 + 품질 지표
- ai_insights.py:     주간 인사이트 + 반복 패턴 + 개선 권고
- container_alerts.py: Docker 이상 감지 → SR 자동 생성
- ncloud.py:          NCloud API (서버/LB/스토리지/비용)
- billing.py:         구독 플랜 + 사용량 측정 + 청구서
- servicenow.py:      ServiceNow CMDB/Incident 양방향 연동
- erp_connector.py:   그룹웨어/HR ERP 연동 + 결재 웹훅
- kakao_notify.py:    카카오 알림톡 + 대량 발송
- auto_report.py:     Excel/PDF 보고서 자동 생성·다운로드
- benchmark.py:       기관 간 익명 벤치마킹 (완전 익명화)
- cohort_analysis.py: 도입 코호트 + 리텐션 + 기능 도입률

DB 모델 (14개 신규 테이블):
tb_learning_run, tb_container_alert_{rule,log},
tb_ncloud_config, tb_subscription, tb_invoice,
tb_servicenow_{config,mapping}, tb_erp_config,
tb_kakao_{config,notify_log}, tb_report_{record,schedule},
tb_benchmark_contrib

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 06:06:59 +09:00

152 lines
6.5 KiB
Python

"""
ServiceNow 연동 커넥터
기능:
- ServiceNow CMDB CI 목록 조회
- Incident 양방향 동기화
- GUARDiA SR → ServiceNow Incident 생성
- ServiceNow Change Request 조회
엔드포인트:
POST /api/servicenow/config — 연동 설정
GET /api/servicenow/config — 설정 조회
POST /api/servicenow/test — 연결 테스트
GET /api/servicenow/incidents — Incident 목록
POST /api/servicenow/sync/{sr_id} — SR → ServiceNow Incident 생성
GET /api/servicenow/cmdb — CMDB CI 목록
GET /api/servicenow/changes — Change Request 목록
"""
from __future__ import annotations
import json
import logging
from datetime import datetime
from typing import Optional
import httpx
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth import get_current_user, require_admin_role
from database import get_db
from models import User, SRRequest, ServiceNowConfig, ServiceNowMapping # 신규
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/servicenow", tags=["ServiceNow"])
PRIORITY_MAP = {"HIGH": "1", "MEDIUM": "2", "LOW": "3"}
class SNowConfigCreate(BaseModel):
instance_url: str = Field(..., description="https://company.service-now.com")
username: str
password: str
assignment_group: Optional[str] = None
async def _snow_request(cfg: ServiceNowConfig, method: str, path: str,
payload: Optional[dict] = None) -> Optional[dict]:
url = f"{cfg.instance_url.rstrip('/')}/api/now/{path}"
try:
async with httpx.AsyncClient(timeout=15, verify=False) as client:
r = await getattr(client, method.lower())(
url, json=payload,
auth=(cfg.username, cfg.password_enc),
headers={"Content-Type": "application/json", "Accept": "application/json"}
)
return r.json() if r.status_code in (200, 201) else None
except Exception as e:
logger.error(f"ServiceNow API 실패: {e}")
return None
@router.post("/config")
async def save_config(
req: SNowConfigCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin_role),
):
row = await db.execute(select(ServiceNowConfig).where(ServiceNowConfig.tenant_id == user.tenant_id))
cfg = row.scalar_one_or_none()
if cfg:
cfg.instance_url = req.instance_url; cfg.username = req.username
cfg.password_enc = req.password; cfg.assignment_group = req.assignment_group
else:
cfg = ServiceNowConfig(
tenant_id=user.tenant_id, instance_url=req.instance_url,
username=req.username, password_enc=req.password,
assignment_group=req.assignment_group, is_active=True, created_at=datetime.utcnow()
)
db.add(cfg)
await db.commit()
return {"ok": True}
@router.post("/test")
async def test_connection(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
row = await db.execute(select(ServiceNowConfig).where(ServiceNowConfig.tenant_id == user.tenant_id))
cfg = row.scalar_one_or_none()
if not cfg: raise HTTPException(404, "설정 없음")
data = await _snow_request(cfg, "GET", "table/sys_user?sysparm_limit=1")
return {"ok": bool(data), "instance": cfg.instance_url}
@router.get("/incidents")
async def list_incidents(limit: int = 20, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
row = await db.execute(select(ServiceNowConfig).where(ServiceNowConfig.tenant_id == user.tenant_id))
cfg = row.scalar_one_or_none()
if not cfg: raise HTTPException(404, "설정 없음")
data = await _snow_request(cfg, "GET", f"table/incident?sysparm_limit={limit}&sysparm_fields=number,short_description,state,priority,opened_at")
records = (data or {}).get("result", [])
return [{"number": r.get("number"), "title": r.get("short_description"),
"state": r.get("state"), "priority": r.get("priority"), "opened_at": r.get("opened_at")} for r in records]
@router.post("/sync/{sr_id}")
async def sync_to_servicenow(sr_id: int, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
"""SR → ServiceNow Incident 생성."""
sr_row = await db.execute(select(SRRequest).where(SRRequest.id == sr_id))
sr = sr_row.scalar_one_or_none()
if not sr: raise HTTPException(404, "SR 없음")
cfg_row = await db.execute(select(ServiceNowConfig).where(ServiceNowConfig.tenant_id == user.tenant_id))
cfg = cfg_row.scalar_one_or_none()
if not cfg: raise HTTPException(404, "ServiceNow 설정 없음")
payload = {
"short_description": f"[GUARDiA SR-{sr.id}] {sr.title}",
"description": sr.description or "",
"impact": PRIORITY_MAP.get((sr.priority or "MEDIUM").upper(), "2"),
"urgency": PRIORITY_MAP.get((sr.priority or "MEDIUM").upper(), "2"),
}
if cfg.assignment_group:
payload["assignment_group"] = {"name": cfg.assignment_group}
data = await _snow_request(cfg, "POST", "table/incident", payload)
if data and data.get("result"):
sn_number = data["result"].get("number", "")
mapping = ServiceNowMapping(sr_id=sr.id, snow_number=sn_number, config_id=cfg.id, synced_at=datetime.utcnow())
db.add(mapping)
await db.commit()
return {"ok": True, "snow_number": sn_number}
raise HTTPException(500, "ServiceNow Incident 생성 실패")
@router.get("/cmdb")
async def list_cmdb(limit: int = 50, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
row = await db.execute(select(ServiceNowConfig).where(ServiceNowConfig.tenant_id == user.tenant_id))
cfg = row.scalar_one_or_none()
if not cfg: raise HTTPException(404, "설정 없음")
data = await _snow_request(cfg, "GET", f"table/cmdb_ci_server?sysparm_limit={limit}&sysparm_fields=name,ip_address,os,status")
return (data or {}).get("result", [])
@router.get("/changes")
async def list_changes(limit: int = 20, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
row = await db.execute(select(ServiceNowConfig).where(ServiceNowConfig.tenant_id == user.tenant_id))
cfg = row.scalar_one_or_none()
if not cfg: raise HTTPException(404, "설정 없음")
data = await _snow_request(cfg, "GET", f"table/change_request?sysparm_limit={limit}&sysparm_fields=number,short_description,state,type,scheduled_start_date")
return (data or {}).get("result", [])