guardia-itsm/models.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

253 lines
8.0 KiB
Python

"""
ORM models + Pydantic schemas for GUARDiA ITSM
"""
from __future__ import annotations
import hashlib
import json
from datetime import datetime
from enum import Enum
from typing import Any, Optional
from pydantic import BaseModel, ConfigDict
from sqlalchemy import (
Boolean, Column, DateTime, ForeignKey, Integer, String, Text, func
)
from sqlalchemy.orm import relationship
from database import Base
# ── Enums ──────────────────────────────────────────────────────────────────────
class SRStatus(str, Enum):
RECEIVED = "RECEIVED"
PARSED = "PARSED"
PENDING_APPROVAL = "PENDING_APPROVAL"
APPROVED = "APPROVED"
IN_PROGRESS = "IN_PROGRESS"
PENDING_PM_VALIDATION = "PENDING_PM_VALIDATION"
COMPLETED = "COMPLETED"
FAILED_ROLLBACK = "FAILED_ROLLBACK"
REJECTED = "REJECTED"
class SRType(str, Enum):
DEPLOY = "DEPLOY"
RESTART = "RESTART"
LOG = "LOG"
INQUIRY = "INQUIRY"
OTHER = "OTHER"
class Priority(str, Enum):
CRITICAL = "CRITICAL"
HIGH = "HIGH"
MEDIUM = "MEDIUM"
LOW = "LOW"
class ApprovalResult(str, Enum):
PENDING = "PENDING"
APPROVED = "APPROVED"
REJECTED = "REJECTED"
# ── ORM Models ─────────────────────────────────────────────────────────────────
class Institution(Base):
__tablename__ = "tb_inst_meta"
id = Column(Integer, primary_key=True, index=True)
inst_code = Column(String(20), unique=True, nullable=False, index=True)
inst_name = Column(String(100), nullable=False)
org_type = Column(String(50))
contact_pm = Column(String(100))
created_at = Column(DateTime, default=func.now())
servers = relationship("Server", back_populates="institution")
sr_requests = relationship("SRRequest", back_populates="institution")
class Server(Base):
__tablename__ = "tb_server_info"
id = Column(Integer, primary_key=True, index=True)
inst_id = Column(Integer, ForeignKey("tb_inst_meta.id"), nullable=False)
server_name = Column(String(100), nullable=False)
server_role = Column(String(20)) # WEB / WAS / DB
os_type = Column(String(50))
os_version = Column(String(50))
ip_addr = Column(String(45)) # NOT exposed in API responses
ssh_user = Column(String(50)) # NOT exposed
os_pw_enc = Column(Text) # AES-256 encrypted, NEVER exposed
port = Column(Integer, default=22)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=func.now())
institution = relationship("Institution", back_populates="servers")
class SRRequest(Base):
__tablename__ = "tb_sr_request"
id = Column(Integer, primary_key=True, index=True)
sr_id = Column(String(30), unique=True, nullable=False, index=True)
inst_id = Column(Integer, ForeignKey("tb_inst_meta.id"))
sr_type = Column(String(20), default=SRType.OTHER)
title = Column(String(200), nullable=False)
description = Column(Text)
status = Column(String(30), default=SRStatus.RECEIVED)
priority = Column(String(20), default=Priority.MEDIUM)
requested_by = Column(String(100))
assigned_to = Column(String(100))
target_server = Column(String(100))
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
institution = relationship("Institution", back_populates="sr_requests")
approvals = relationship("ApprovalFlow", back_populates="sr_request")
audit_logs = relationship("AuditLog", back_populates="sr_request")
class ApprovalFlow(Base):
__tablename__ = "tb_approval_flow"
id = Column(Integer, primary_key=True, index=True)
sr_id = Column(String(30), ForeignKey("tb_sr_request.sr_id"), nullable=False)
approver = Column(String(100), nullable=False)
result = Column(String(20), default=ApprovalResult.PENDING)
comment = Column(Text)
decided_at = Column(DateTime)
created_at = Column(DateTime, default=func.now())
sr_request = relationship("SRRequest", back_populates="approvals")
class AuditLog(Base):
__tablename__ = "tb_audit_log"
id = Column(Integer, primary_key=True, index=True)
sr_id = Column(String(30), ForeignKey("tb_sr_request.sr_id"))
actor = Column(String(100))
action = Column(String(100), nullable=False)
detail = Column(Text)
prev_hash = Column(String(64))
log_hash = Column(String(64))
created_at = Column(DateTime, default=func.now())
sr_request = relationship("SRRequest", back_populates="audit_logs")
class OpsTask(Base):
__tablename__ = "tb_ops_task"
id = Column(Integer, primary_key=True, index=True)
sr_id = Column(String(30), ForeignKey("tb_sr_request.sr_id"))
task_name = Column(String(200), nullable=False)
task_order = Column(Integer, default=0)
status = Column(String(30), default="PENDING")
result_msg = Column(Text)
started_at = Column(DateTime)
finished_at = Column(DateTime)
created_at = Column(DateTime, default=func.now())
# ── Pydantic Schemas ───────────────────────────────────────────────────────────
class InstitutionOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
inst_code: str
inst_name: str
org_type: Optional[str]
contact_pm: Optional[str]
class ServerOut(BaseModel):
"""Server info — sensitive fields (ip_addr, ssh_user, os_pw_enc) intentionally omitted."""
model_config = ConfigDict(from_attributes=True)
id: int
server_name: str
server_role: Optional[str]
os_type: Optional[str]
is_active: bool
class SRCreate(BaseModel):
title: str
description: Optional[str] = None
sr_type: SRType = SRType.OTHER
priority: Priority = Priority.MEDIUM
requested_by: str
assigned_to: Optional[str] = None
target_server: Optional[str] = None
inst_code: Optional[str] = None
class SROut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
sr_id: str
sr_type: str
title: str
description: Optional[str]
status: str
priority: str
requested_by: str
assigned_to: Optional[str]
target_server: Optional[str]
created_at: datetime
updated_at: datetime
class SRStatusUpdate(BaseModel):
status: SRStatus
actor: str
comment: Optional[str] = None
class ApprovalCreate(BaseModel):
approver: str
result: ApprovalResult
comment: Optional[str] = None
class ApprovalOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
sr_id: str
approver: str
result: str
comment: Optional[str]
decided_at: Optional[datetime]
created_at: datetime
class AuditLogOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
sr_id: Optional[str]
actor: Optional[str]
action: str
detail: Optional[str]
log_hash: Optional[str]
created_at: datetime
# ── Audit hash helper ──────────────────────────────────────────────────────────
def compute_log_hash(prev_hash: Optional[str], actor: str, action: str,
detail: str, ts: str) -> str:
payload = json.dumps(
{"prev": prev_hash or "", "actor": actor, "action": action,
"detail": detail, "ts": ts},
ensure_ascii=False, sort_keys=True
)
return hashlib.sha256(payload.encode()).hexdigest()