guardia-itsm/core/seed.py
DESKTOP-TKLFCPRython bc85c5228a feat(itsm): Jira-like ITSM 시스템 구현
- FastAPI + SQLAlchemy(aiosqlite) 기반 SR 상태 머신
  (RECEIVED → PARSED → PENDING_APPROVAL → APPROVED → IN_PROGRESS
   → PENDING_PM_VALIDATION → COMPLETED / FAILED_ROLLBACK)
- PM 승인 워크플로우 (ApprovalFlow 테이블)
- SHA-256 해시 체인 감사 로그 (위변조 방지)
- AES-256-GCM 서버 자격증명 암호화 (IP/PW API 미노출)
- CMDB: 기관(MOF/MOIS/MSS) + 서버 정보 관리
- 더미 데이터 자동 시딩 (6개 SR, 3개 기관, 6개 서버)
- Dark-theme SPA: 대시보드 / 칸반 보드 / SR 목록 / 감사 로그 / CMDB

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:31:09 +09:00

206 lines
10 KiB
Python

"""Seed dummy data for GUARDiA ITSM demo."""
import base64
import os
from datetime import datetime, timedelta
from uuid import uuid4
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from sqlalchemy.ext.asyncio import AsyncSession
from models import (
ApprovalFlow, ApprovalResult, AuditLog, Institution, OpsTask,
Priority, SRRequest, SRStatus, SRType, Server, compute_log_hash
)
def _encrypt_pw(plain: str) -> str:
"""AES-256-GCM encrypt. Key from env (demo: fixed 32-byte key)."""
key = os.environ.get("GUARDIA_ENC_KEY", "guardia-demo-key-32bytes-padding!").encode()[:32]
key = key.ljust(32, b"\x00")
aesgcm = AESGCM(key)
nonce = os.urandom(12)
ct = aesgcm.encrypt(nonce, plain.encode(), None)
return base64.b64encode(nonce + ct).decode()
def _new_sr() -> str:
return f"SR-{datetime.now().strftime('%Y%m%d')}-{str(uuid4())[:6].upper()}"
async def seed_all(db: AsyncSession) -> None:
from sqlalchemy import select
result = await db.execute(select(Institution))
if result.scalars().first():
return # already seeded
# ── Institutions ────────────────────────────────────────────
inst_data = [
{"inst_code": "MOF", "inst_name": "기획재정부", "org_type": "중앙행정기관", "contact_pm": "김PM"},
{"inst_code": "MOIS", "inst_name": "행정안전부", "org_type": "중앙행정기관", "contact_pm": "이PM"},
{"inst_code": "MSS", "inst_name": "중소벤처기업부", "org_type": "중앙행정기관", "contact_pm": "박PM"},
]
institutions = []
for d in inst_data:
inst = Institution(**d)
db.add(inst)
institutions.append(inst)
await db.flush()
# ── Servers ─────────────────────────────────────────────────
server_data = [
# MOF
{"inst": institutions[0], "server_name": "MOF-WEB-01", "server_role": "WEB",
"os_type": "RHEL", "os_version": "8.9", "ip_addr": "10.10.1.11",
"ssh_user": "opsagent", "os_pw_enc": _encrypt_pw("DEMO_MASKED"), "port": 22},
{"inst": institutions[0], "server_name": "MOF-WAS-01", "server_role": "WAS",
"os_type": "RHEL", "os_version": "8.9", "ip_addr": "10.10.1.21",
"ssh_user": "opsagent", "os_pw_enc": _encrypt_pw("DEMO_MASKED"), "port": 22},
{"inst": institutions[0], "server_name": "MOF-DB-01", "server_role": "DB",
"os_type": "RHEL", "os_version": "8.9", "ip_addr": "10.10.1.31",
"ssh_user": "opsagent", "os_pw_enc": _encrypt_pw("DEMO_MASKED"), "port": 22},
# MOIS
{"inst": institutions[1], "server_name": "MOIS-WEB-01", "server_role": "WEB",
"os_type": "CentOS", "os_version": "7.9", "ip_addr": "10.20.1.11",
"ssh_user": "opsagent", "os_pw_enc": _encrypt_pw("DEMO_MASKED"), "port": 22},
{"inst": institutions[1], "server_name": "MOIS-WAS-01", "server_role": "WAS",
"os_type": "CentOS", "os_version": "7.9", "ip_addr": "10.20.1.21",
"ssh_user": "opsagent", "os_pw_enc": _encrypt_pw("DEMO_MASKED"), "port": 22},
# MSS
{"inst": institutions[2], "server_name": "MSS-WAS-01", "server_role": "WAS",
"os_type": "Ubuntu", "os_version": "22.04", "ip_addr": "10.30.1.21",
"ssh_user": "opsagent", "os_pw_enc": _encrypt_pw("DEMO_MASKED"), "port": 22},
]
for sd in server_data:
inst = sd.pop("inst")
srv = Server(inst_id=inst.id, **sd)
db.add(srv)
await db.flush()
# ── SR Requests ─────────────────────────────────────────────
now = datetime.now()
sr_data = [
{
"sr_id": "SR-20260524-AA1B2C", "inst": institutions[0],
"sr_type": SRType.DEPLOY, "title": "기재부 예산시스템 WAS 배포",
"description": "2026년 2차 추경 예산시스템 class 파일 배포",
"status": SRStatus.COMPLETED, "priority": Priority.HIGH,
"requested_by": "홍길동", "assigned_to": "운영팀",
"target_server": "MOF-WAS-01",
"created_at": now - timedelta(days=3),
"updated_at": now - timedelta(days=2),
},
{
"sr_id": "SR-20260524-BB3C4D", "inst": institutions[0],
"sr_type": SRType.RESTART, "title": "기재부 WAS-02 재기동 요청",
"description": "OutOfMemoryError 발생으로 WAS 재기동 필요",
"status": SRStatus.PENDING_APPROVAL, "priority": Priority.CRITICAL,
"requested_by": "김운영", "assigned_to": "운영팀",
"target_server": "MOF-WAS-01",
"created_at": now - timedelta(hours=2),
"updated_at": now - timedelta(hours=1),
},
{
"sr_id": "SR-20260524-CC5D6E", "inst": institutions[1],
"sr_type": SRType.DEPLOY, "title": "행안부 민원포털 정적파일 배포",
"description": "UI 개선 HTML/JS/CSS 정적 파일 배포",
"status": SRStatus.IN_PROGRESS, "priority": Priority.MEDIUM,
"requested_by": "이배포", "assigned_to": "운영팀",
"target_server": "MOIS-WEB-01",
"created_at": now - timedelta(hours=5),
"updated_at": now - timedelta(minutes=30),
},
{
"sr_id": "SR-20260524-DD7E8F", "inst": institutions[2],
"sr_type": SRType.LOG, "title": "중기부 WAS 에러 로그 분석",
"description": "Connection pool exhausted 오류 원인 분석 요청",
"status": SRStatus.RECEIVED, "priority": Priority.MEDIUM,
"requested_by": "박운영", "assigned_to": None,
"target_server": "MSS-WAS-01",
"created_at": now - timedelta(minutes=20),
"updated_at": now - timedelta(minutes=20),
},
{
"sr_id": "SR-20260524-EE9F0A", "inst": institutions[0],
"sr_type": SRType.INQUIRY, "title": "기재부 SSL 인증서 만료 갱신",
"description": "portal.mof.go.kr SSL 인증서 D-14 갱신 요청",
"status": SRStatus.APPROVED, "priority": Priority.HIGH,
"requested_by": "최보안", "assigned_to": "보안팀",
"target_server": "MOF-WEB-01",
"created_at": now - timedelta(days=1),
"updated_at": now - timedelta(hours=3),
},
{
"sr_id": "SR-20260524-FF1A2B", "inst": institutions[1],
"sr_type": SRType.RESTART, "title": "행안부 WAS 롤링 재기동",
"description": "주간 정기 점검 롤링 재기동",
"status": SRStatus.FAILED_ROLLBACK, "priority": Priority.LOW,
"requested_by": "이운영", "assigned_to": "운영팀",
"target_server": "MOIS-WAS-01",
"created_at": now - timedelta(days=2),
"updated_at": now - timedelta(days=1),
},
]
sr_objs = []
for sd in sr_data:
inst = sd.pop("inst")
sr = SRRequest(inst_id=inst.id, **sd)
db.add(sr)
sr_objs.append(sr)
await db.flush()
# ── Approvals ────────────────────────────────────────────────
approval_data = [
{"sr": sr_objs[0], "approver": "김PM", "result": ApprovalResult.APPROVED,
"comment": "정상 배포 승인", "decided_at": now - timedelta(days=2, hours=22)},
{"sr": sr_objs[1], "approver": "김PM", "result": ApprovalResult.PENDING,
"comment": None, "decided_at": None},
{"sr": sr_objs[4], "approver": "김PM", "result": ApprovalResult.APPROVED,
"comment": "긴급 갱신 승인", "decided_at": now - timedelta(hours=4)},
{"sr": sr_objs[5], "approver": "이PM", "result": ApprovalResult.APPROVED,
"comment": "정기 점검 승인", "decided_at": now - timedelta(days=1, hours=20)},
]
for ad in approval_data:
sr = ad.pop("sr")
apv = ApprovalFlow(sr_id=sr.sr_id, **ad)
db.add(apv)
await db.flush()
# ── Audit Logs with hash chain ───────────────────────────────
prev_hash = None
audit_entries = [
{"sr": sr_objs[0], "actor": "홍길동", "action": "SR_CREATED", "detail": "배포 SR 생성"},
{"sr": sr_objs[0], "actor": "김PM", "action": "SR_APPROVED", "detail": "PM 승인"},
{"sr": sr_objs[0], "actor": "system", "action": "SR_COMPLETED","detail": "배포 완료"},
{"sr": sr_objs[1], "actor": "김운영", "action": "SR_CREATED", "detail": "재기동 SR 생성"},
{"sr": sr_objs[2], "actor": "이배포", "action": "SR_CREATED", "detail": "정적파일 배포 SR 생성"},
{"sr": sr_objs[2], "actor": "system", "action": "SR_STARTED", "detail": "배포 작업 시작"},
]
ts_base = now - timedelta(days=3)
for i, ae in enumerate(audit_entries):
sr = ae.pop("sr")
ts = (ts_base + timedelta(hours=i * 4)).isoformat()
log_hash = compute_log_hash(prev_hash, ae["actor"], ae["action"], ae["detail"], ts)
log = AuditLog(
sr_id=sr.sr_id, prev_hash=prev_hash, log_hash=log_hash,
created_at=ts_base + timedelta(hours=i * 4), **ae
)
db.add(log)
prev_hash = log_hash
await db.flush()
# ── OPS Tasks ────────────────────────────────────────────────
task_data = [
{"sr": sr_objs[0], "task_name": "파일 전송 (SFTP)", "task_order": 1, "status": "COMPLETED"},
{"sr": sr_objs[0], "task_name": "WAS 롤링 재기동", "task_order": 2, "status": "COMPLETED"},
{"sr": sr_objs[0], "task_name": "헬스체크 확인", "task_order": 3, "status": "COMPLETED"},
{"sr": sr_objs[2], "task_name": "파일 전송 (SFTP)", "task_order": 1, "status": "COMPLETED"},
{"sr": sr_objs[2], "task_name": "Nginx 설정 리로드", "task_order": 2, "status": "IN_PROGRESS"},
]
for td in task_data:
sr = td.pop("sr")
task = OpsTask(sr_id=sr.sr_id, **td)
db.add(task)
await db.commit()