- 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>
206 lines
10 KiB
Python
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()
|