guardia-itsm/core/seed.py
DESKTOP-TKLFCPRython 64c27c3509 feat(itsm): G-1~G-12 확장 기능 + 하네스/봇/설치스크립트 구현
G-1: 메신저 Webhook Relay + _send_to_room 실제 httpx 호출 구현
G-2: POST /api/tasks/bulk SR 대량작업 엔드포인트 (최대 100건)
G-3: 라이선스 만료 알림 스케줄러 (매일 09:00 KST)
G-4: 체험판 upgrade_banner 필드 + license.py 배너 로직
G-5: core/auto_rca.py + incidents/problem auto-rca 엔드포인트
G-6: core/deploy_impact.py + vibe impact-analysis 엔드포인트
G-7: core/ticket_classifier.py + SR 생성 시 AI 분류 + ai-suggestion API
G-8: VulnPatchRecord 모델 + vuln_scan 패치추적 4개 엔드포인트
G-9: core/jira_sync.py + gateway Jira/Confluence 연동 엔드포인트
G-10: core/push_notify.py + routers/push.py + PushSubscription 모델
G-11: approvals 다중승인 (위임/서명/기한초과/마감연장)
G-12: alembic.ini + migrations/ + cicd/migrate_to_postgres.sh

하네스: guardia-orchestrator 확장기능 Phase 반영
봇명령어: /sr /status /license /bulk 슬래시 명령어 추가
설치스크립트: setup/ (Ubuntu, CentOS, RHEL, Windows) --test 옵션 포함

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:18:52 +09:00

1088 lines
54 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 sqlalchemy import select
from models import (
ApprovalFlow, ApprovalResult, AuditLog, Institution, InstContact, OpsTask,
Priority, SRRequest, SRStatus, SRType, Server, User, UserRole, 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_users(db: AsyncSession) -> None:
"""초기 사용자 생성 (없을 때만). 모두 초기 비밀번호 1111."""
from core.auth import hash_password
result = await db.execute(select(User))
if result.scalars().first():
return # already seeded
hashed = hash_password("1111")
users = [
# admin — 최초 로그인 후 변경 불필요 (데모 편의)
User(username="admin", display_name="관리자", role=UserRole.ADMIN,
hashed_pw=hashed, must_change_pw=False),
User(username="engineer1", display_name="김엔지니어", role=UserRole.ENGINEER,
hashed_pw=hashed, must_change_pw=True),
User(username="engineer2", display_name="이엔지니어", role=UserRole.ENGINEER,
hashed_pw=hashed, must_change_pw=True),
User(username="pm1", display_name="김PM", role=UserRole.PM,
hashed_pw=hashed, must_change_pw=True),
User(username="customer1", display_name="기재부 담당자", role=UserRole.CUSTOMER,
hashed_pw=hashed, must_change_pw=True, inst_code="MOF"),
User(username="customer2", display_name="행안부 담당자", role=UserRole.CUSTOMER,
hashed_pw=hashed, must_change_pw=True, inst_code="MOIS"),
]
for u in users:
db.add(u)
await db.commit() # 사용자 즉시 커밋 (seed_all이 조기 return해도 보존)
async def _seed_engineer_profiles(db: AsyncSession) -> None:
"""엔지니어 스킬 프로필 생성 (없을 때만)."""
from models import EngineerProfile
result = await db.execute(select(EngineerProfile))
if result.scalars().first():
return
profiles = [
EngineerProfile(
username="engineer1", display_name="김엔지니어",
skill_types="DEPLOY,RESTART",
inst_affinity="MOF",
max_workload=5, is_available=True,
),
EngineerProfile(
username="engineer2", display_name="이엔지니어",
skill_types="LOG,INQUIRY,DEPLOY",
inst_affinity="MOIS,MSS",
max_workload=5, is_available=True,
),
]
for p in profiles:
db.add(p)
await db.commit()
async def _seed_kb_documents(db: AsyncSession) -> None:
"""기술 지식 문서 시드 (없을 때만)."""
from models import KBDocument
result = await db.execute(select(KBDocument))
if result.scalars().first():
return
docs = [
KBDocument(
doc_id="KB-001", category="JAVA",
title="Java OutOfMemoryError — GC overhead limit exceeded",
symptoms=(
"WAS 로그에 java.lang.OutOfMemoryError: GC overhead limit exceeded 발생. "
"힙 메모리 사용률 95% 이상 지속. 응답 지연 및 504 오류 동반."
),
cause=(
"힙 영역 부족으로 GC가 전체 시간의 98% 이상을 소비. "
"메모리 누수(Memory Leak), 세션 객체 미반환, 캐시 미제한 등이 주요 원인."
),
solution=(
"1. WAS 재기동으로 임시 복구\n"
"2. 힙 덤프 분석: jmap -dump:format=b,file=heap.hprof <PID>\n"
"3. Eclipse MAT 또는 VisualVM으로 누수 객체 식별\n"
"4. JVM 옵션 조정: -Xmx 증가, -XX:+UseG1GC 적용\n"
"5. 세션 타임아웃·캐시 정책 점검"
),
commands="jps -l\njmap -heap <PID>\ntop -H -p <PID>",
tags="oom outofmemory gc heap java jvm was 메모리누수 재기동",
sr_type="RESTART",
),
KBDocument(
doc_id="KB-002", category="MIDDLEWARE",
title="Connection Pool Exhausted — JDBC 커넥션 풀 고갈",
symptoms=(
"로그에 Connection pool exhausted (200/200) 또는 "
"Unable to acquire JDBC Connection 오류. DB 응답은 정상이나 연결 대기 타임아웃."
),
cause=(
"DB 커넥션을 반환하지 않는 코드(close() 미호출), "
"트랜잭션 장시간 점유, 또는 pool 최대값 과소 설정."
),
solution=(
"1. 현재 활성 커넥션 확인: SELECT count(*) FROM v$session WHERE status='ACTIVE'\n"
"2. WAS 재기동으로 임시 복구\n"
"3. connection-timeout, max-pool-size 설정 조정\n"
"4. HikariCP 누수 감지: leakDetectionThreshold=30000 설정\n"
"5. 애플리케이션 코드에서 try-with-resources 사용 여부 점검"
),
commands=(
"# Tomcat JDBC pool 상태\n"
"curl http://localhost:8080/manager/jmxproxy/?get=Catalina:type=DataSource"
),
tags="connection pool jdbc hikari dbcp exhausted 커넥션풀 db 데이터베이스",
sr_type="LOG",
),
KBDocument(
doc_id="KB-003", category="DB",
title="ORA-01555 — Snapshot too old (Oracle 스냅샷 오류)",
symptoms=(
"ORA-01555: snapshot too old: rollback segment number with name \"\" too small. "
"장시간 실행 쿼리 또는 배치 작업 도중 발생."
),
cause=(
"쿼리 실행 중 UNDO 세그먼트가 다른 트랜잭션에 의해 덮어씌워져 "
"일관된 읽기(Read Consistency)를 보장할 수 없는 상태. "
"UNDO_RETENTION 설정이 쿼리 실행 시간보다 짧은 경우 빈번 발생."
),
solution=(
"1. UNDO_RETENTION 파라미터 증가\n"
" ALTER SYSTEM SET UNDO_RETENTION=3600;\n"
"2. 장시간 쿼리 최적화 (실행계획 분석)\n"
"3. AUTOEXTEND ON 설정으로 UNDO 테이블스페이스 자동 확장\n"
"4. 대용량 배치 작업은 청크 단위로 COMMIT 처리"
),
commands=(
"-- UNDO 설정 확인\n"
"SELECT name, value FROM v$parameter WHERE name LIKE 'undo%';\n"
"-- 활성 트랜잭션 확인\n"
"SELECT s.sid, s.sql_id, s.status FROM v$session s WHERE s.status='ACTIVE';"
),
tags="ora-01555 snapshot oracle undo rollback db 오라클 스냅샷",
sr_type="LOG",
),
KBDocument(
doc_id="KB-004", category="SECURITY",
title="SSL/TLS 인증서 만료 — 갱신 절차",
symptoms=(
"브라우저에서 'NET::ERR_CERT_DATE_INVALID' 오류. "
"curl 요청 시 SSL certificate problem: certificate has expired. "
"인증서 만료 D-30, D-14 모니터링 알림."
),
cause="SSL/TLS 인증서 유효 기간 만료 또는 임박.",
solution=(
"1. 현재 인증서 확인:\n"
" openssl s_client -connect hostname:443 </dev/null 2>/dev/null | openssl x509 -noout -dates\n"
"2. 새 인증서 발급 (CA 또는 Let's Encrypt)\n"
"3. Nginx 설정 갱신:\n"
" ssl_certificate /etc/ssl/certs/new.crt;\n"
" ssl_certificate_key /etc/ssl/private/new.key;\n"
"4. nginx -t && systemctl reload nginx\n"
"5. 갱신 후 브라우저에서 인증서 유효성 재확인"
),
commands=(
"openssl s_client -connect HOST:443 </dev/null 2>/dev/null | openssl x509 -noout -dates\n"
"openssl x509 -in cert.crt -noout -text | grep -A2 Validity"
),
tags="ssl tls 인증서 certificate 만료 갱신 https nginx 보안",
sr_type="INQUIRY",
),
KBDocument(
doc_id="KB-005", category="WEB",
title="Nginx 502 Bad Gateway — WAS 연결 실패",
symptoms=(
"브라우저 또는 API 클라이언트에서 502 Bad Gateway 응답. "
"Nginx 에러 로그: connect() failed (111: Connection refused) "
"또는 upstream timed out."
),
cause=(
"Nginx upstream(WAS/Tomcat)이 응답하지 않는 상태. "
"WAS 프로세스 다운, 포트 불일치, 또는 WAS 과부하로 인한 타임아웃."
),
solution=(
"1. WAS 프로세스 확인: systemctl status tomcat9\n"
"2. WAS 포트 리슨 확인: ss -tlnp | grep 8080\n"
"3. WAS 재기동: systemctl restart tomcat9\n"
"4. Nginx upstream 설정 확인: /etc/nginx/conf.d/*.conf\n"
"5. proxy_connect_timeout, proxy_read_timeout 조정\n"
"6. WAS 힙 메모리 및 스레드 풀 상태 점검"
),
commands=(
"systemctl status tomcat9\n"
"ss -tlnp | grep 8080\n"
"tail -n 100 /var/log/nginx/error.log | grep 'upstream'"
),
tags="nginx 502 upstream gateway was tomcat 연결실패 proxy 웹서버",
sr_type="RESTART",
),
KBDocument(
doc_id="KB-006", category="OS",
title="디스크 용량 부족 — 로그 파일 정리",
symptoms=(
"df -h 결과 /app 또는 /var 파티션 사용률 90% 이상. "
"WAS 로그 파일 무제한 증가. 신규 파일 쓰기 실패 오류."
),
cause=(
"로그 로테이션 미설정, 오래된 백업 파일 미삭제, "
"힙 덤프 파일 누적 등으로 디스크 용량 초과."
),
solution=(
"1. 대용량 파일 파악: du -sh /app/logs/* | sort -rh | head -20\n"
"2. 오래된 로그 삭제: find /app/logs -name '*.log' -mtime +30 -delete\n"
"3. 힙 덤프 파일 제거: find / -name '*.hprof' -delete\n"
"4. logrotate 설정: /etc/logrotate.d/ 에 rotate 정책 추가\n"
"5. 디스크 사용량 모니터링 임계치 설정 (80% 경보)"
),
commands=(
"df -h\n"
"du -sh /app/logs/* | sort -rh | head -20\n"
"find /app/logs -name '*.log' -mtime +30 -exec ls -lh {} \\;"
),
tags="disk 디스크 용량 storage 로그 logrotate 파일시스템 파티션 os 리눅스",
sr_type="INQUIRY",
),
KBDocument(
doc_id="KB-007", category="JAVA",
title="Tomcat 클래스 배포 후 재기동 없이 반영 안 됨",
symptoms=(
"SFTP로 class 파일 전송 후에도 변경 사항이 반영되지 않음. "
"이전 로직으로 계속 동작."
),
cause=(
"Tomcat이 클래스 파일을 JVM 메모리에 캐시 중. "
"Hot deploy 미지원 또는 reloadable=false 설정."
),
solution=(
"1. Tomcat 롤링 재기동 (서비스 중단 최소화):\n"
" systemctl stop tomcat9 && sleep 3 && systemctl start tomcat9\n"
"2. work 디렉토리 초기화:\n"
" rm -rf $CATALINA_HOME/work/Catalina/\n"
"3. 무중단 배포 구성: 2대 이상 WAS를 순차 재기동\n"
"4. 향후: Spring DevTools 또는 JRebel 도입 검토"
),
commands=(
"systemctl stop tomcat9\n"
"rm -rf /opt/tomcat/work/Catalina/localhost/\n"
"systemctl start tomcat9\n"
"curl -sf http://localhost:8080/health | python3 -m json.tool"
),
tags="tomcat 배포 deploy class 재기동 restart 캐시 was 클래스파일",
sr_type="DEPLOY",
),
KBDocument(
doc_id="KB-008", category="OS",
title="Linux OOM Killer — 프로세스 강제 종료",
symptoms=(
"dmesg 로그에 'Out of memory: Kill process' 메시지. "
"WAS 또는 DB 프로세스가 이유 없이 종료됨."
),
cause=(
"물리 메모리 + 스왑 공간 고갈 시 커널의 OOM Killer가 "
"가장 많은 메모리를 사용하는 프로세스를 강제 종료."
),
solution=(
"1. OOM 발생 확인: dmesg | grep -i 'oom\\|killed'\n"
"2. 메모리 사용 현황: free -h && cat /proc/meminfo\n"
"3. 스왑 공간 추가 (임시 방편):\n"
" dd if=/dev/zero of=/swapfile bs=1G count=4\n"
" chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile\n"
"4. 근본 해결: JVM 힙 크기 조정 또는 서버 메모리 증설\n"
"5. oom_score_adj 조정으로 주요 프로세스 보호"
),
commands=(
"dmesg | grep -i 'oom\\|killed' | tail -20\n"
"free -h\n"
"ps aux --sort=-%mem | head -10"
),
tags="oom killer linux 메모리 memory 커널 프로세스 종료 swap 스왑",
sr_type="RESTART",
),
]
for doc in docs:
db.add(doc)
await db.commit()
async def _seed_shell_scripts(db: AsyncSession) -> None:
"""쉘 스크립트 라이브러리 시드 데이터."""
from models import ShellScript
r = await db.execute(select(ShellScript))
if r.scalars().first():
return
scripts = [
ShellScript(
script_name="check_infra_health",
category="MONITORING", sub_category="HEALTH_CHECK",
target_layer="ALL", os_type="LINUX",
description="WEB/WAS/DB 서버 Ping 상태 및 서비스 포트 일괄 점검. 서비스 장애 1차 확인용.",
script_body="""#!/bin/bash
# 사용법: ./check_infra_health.sh [WEB|WAS|DB]
# 출력: JSON {"timestamp":"...","results":[...]}
TARGET=${1:-ALL}
check_node() {
local name=$1 ip=$2 port=$3
ping -c 1 -W 1 "${ip}" > /dev/null 2>&1 && ping_s="UP" || ping_s="DOWN"
timeout 2 bash -c "cat < /dev/null > /dev/tcp/${ip}/${port}" 2>/dev/null && port_s="OPEN" || port_s="CLOSED"
echo "{\"server\":\"${name}\",\"ping\":\"${ping_s}\",\"port_status\":\"${port_s}\"}"
}
echo "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"target\":\"${TARGET}\"}"
""",
sample_output='{"timestamp":"2025-01-15T09:00:00Z","results":[{"server":"WAS","ping":"UP","port_status":"OPEN"}]}',
is_dangerous=False, requires_approval=False,
author="system", tags="health check ping port 헬스체크 점검",
),
ShellScript(
script_name="manage_service",
category="SM", sub_category="SERVICE_CONTROL",
target_layer="WAS", os_type="LINUX",
description="WAS(Tomcat/JBoss 등) 서비스 상태 확인·기동·중지·재기동. 롤링 재기동 전 단일 노드 제어용.",
script_body="""#!/bin/bash
# 사용법: ./manage_service.sh [WEB|WAS|DB] [status|start|stop|restart]
TARGET=$1; ACTION=$2
declare -A SERVICE_NAMES=([WEB]="nginx" [WAS]="tomcat9" [DB]="postgresql")
SVC=${SERVICE_NAMES[$TARGET]:-$TARGET}
case ${ACTION} in
status)
RAW=$(systemctl is-active ${SVC} 2>/dev/null)
[ "$RAW" = "active" ] && STATUS="RUNNING" || STATUS="STOPPED"
echo "{\"server\":\"${TARGET}\",\"service\":\"${SVC}\",\"status\":\"${STATUS}\"}"
;;
start|stop|restart)
sudo systemctl ${ACTION} ${SVC} 2>&1
[ $? -eq 0 ] && RESULT="SUCCESS" || RESULT="FAILED"
echo "{\"server\":\"${TARGET}\",\"service\":\"${SVC}\",\"result\":\"${RESULT}\",\"action\":\"${ACTION}\"}"
;;
*)
echo "{\"error\":\"알 수 없는 액션: ${ACTION}\"}"
;;
esac
""",
parameters='[{"name":"TARGET","desc":"대상 레이어 (WEB|WAS|DB)","required":true},{"name":"ACTION","desc":"동작 (status|start|stop|restart)","required":true}]',
is_dangerous=True, requires_approval=True,
author="system", tags="service 서비스 tomcat nginx restart 재기동 start stop",
),
ShellScript(
script_name="monitor_resources",
category="MONITORING", sub_category="RESOURCE_CHECK",
target_layer="ALL", os_type="LINUX",
description="CPU·메모리·디스크 사용률 임계치 감시. 85% 이상 시 WARN, 95% 이상 CRITICAL 출력.",
script_body="""#!/bin/bash
# 출력: JSON 형식의 리소스 사용 현황
CPU=$(top -bn1 | grep "Cpu(s)" | awk '{print 100 - $8}' | cut -d. -f1)
MEM_TOTAL=$(free -m | awk '/^Mem:/{print $2}')
MEM_USED=$(free -m | awk '/^Mem:/{print $3}')
MEM_PCT=$((MEM_USED * 100 / MEM_TOTAL))
DISK=$(df -h / | awk 'NR==2{print $5}' | tr -d '%')
cpu_level="NORMAL"; mem_level="NORMAL"; disk_level="NORMAL"
[ "$CPU" -ge 95 ] && cpu_level="CRITICAL" || [ "$CPU" -ge 85 ] && cpu_level="WARN"
[ "$MEM_PCT" -ge 95 ] && mem_level="CRITICAL" || [ "$MEM_PCT" -ge 85 ] && mem_level="WARN"
[ "$DISK" -ge 95 ] && disk_level="CRITICAL" || [ "$DISK" -ge 85 ] && disk_level="WARN"
echo "{\"cpu_pct\":${CPU},\"cpu_level\":\"${cpu_level}\",\"mem_pct\":${MEM_PCT},\"mem_level\":\"${mem_level}\",\"disk_pct\":${DISK},\"disk_level\":\"${disk_level}\",\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}"
""",
is_dangerous=False, requires_approval=False,
author="system", tags="cpu memory disk 자원 리소스 모니터링 임계치",
),
ShellScript(
script_name="collect_target_logs",
category="SM", sub_category="LOG_ANALYSIS",
target_layer="ALL", os_type="LINUX",
description="WAS/WEB/DB 에러 로그 원격 수집. ERROR·Exception·CRITICAL 키워드 강조 출력.",
script_body="""#!/bin/bash
# 사용법: ./collect_target_logs.sh [로그경로] [라인수(기본100)]
LOG_PATH=${1:-/app/tomcat/logs/catalina.out}
LINES=${2:-100}
OUTPUT=$(tail -n ${LINES} ${LOG_PATH} 2>/dev/null | awk '
/Exception|ERROR|Error|CRITICAL|FATAL/ { print "[ALERT] " $0; next }
{ print $0 }
')
CLEAN=$(echo "$OUTPUT" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))" 2>/dev/null || echo "\"로그 수집 오류\"")
echo "{\"log_path\":\"${LOG_PATH}\",\"lines\":${LINES},\"log_data\":${CLEAN}}"
""",
parameters='[{"name":"LOG_PATH","desc":"로그 파일 절대 경로","required":true},{"name":"LINES","desc":"수집 라인 수 (기본 100)","required":false}]',
is_dangerous=False, requires_approval=False,
author="system", tags="log 로그 error exception 에러 수집 tail grep",
),
ShellScript(
script_name="backup_and_clear",
category="SM", sub_category="LOG_ROTATE",
target_layer="WAS", os_type="LINUX",
description="30일 이상 된 로그 파일 백업 후 삭제. 디스크 여유 확보용. 실행 전 반드시 백업 경로 확인.",
script_body="""#!/bin/bash
# 사용법: ./backup_and_clear.sh [로그디렉토리] [보존일수(기본30)]
LOG_DIR=${1:-/app/tomcat/logs}
KEEP_DAYS=${2:-30}
BACKUP_DIR=${LOG_DIR}/archive/$(date +%Y%m%d)
mkdir -p "${BACKUP_DIR}"
MOVED=0
while IFS= read -r -d '' f; do
mv "$f" "${BACKUP_DIR}/" && MOVED=$((MOVED+1))
done < <(find "${LOG_DIR}" -maxdepth 1 -name "*.log" -mtime +${KEEP_DAYS} -print0 2>/dev/null)
echo "{\"log_dir\":\"${LOG_DIR}\",\"keep_days\":${KEEP_DAYS},\"moved_files\":${MOVED},\"backup_dir\":\"${BACKUP_DIR}\"}"
""",
parameters='[{"name":"LOG_DIR","desc":"정리 대상 로그 디렉토리","required":true},{"name":"KEEP_DAYS","desc":"보존 기간(일), 기본 30","required":false}]',
is_dangerous=True, requires_approval=True,
author="system", tags="backup 백업 log 로그 rotate 정리 disk 디스크",
),
ShellScript(
script_name="ssl_expiry_check",
category="SECURITY", sub_category="SSL_CHECK",
target_layer="WEB", os_type="LINUX",
description="SSL/TLS 인증서 만료일 확인. D-30 미만 시 WARN, D-7 미만 시 CRITICAL 반환.",
script_body="""#!/bin/bash
# 사용법: ./ssl_expiry_check.sh [호스트] [포트(기본443)]
HOST=${1:-localhost}
PORT=${2:-443}
EXPIRY=$(echo | timeout 5 openssl s_client -servername "${HOST}" -connect "${HOST}:${PORT}" 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -z "$EXPIRY" ]; then
echo "{\"host\":\"${HOST}\",\"port\":${PORT},\"status\":\"ERROR\",\"message\":\"인증서 조회 실패\"}"
exit 1
fi
EXPIRY_TS=$(date -d "${EXPIRY}" +%s 2>/dev/null || date -jf "%b %d %T %Y %Z" "${EXPIRY}" +%s 2>/dev/null)
NOW_TS=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_TS - NOW_TS) / 86400 ))
LEVEL="OK"
[ ${DAYS_LEFT} -le 30 ] && LEVEL="WARN"
[ ${DAYS_LEFT} -le 7 ] && LEVEL="CRITICAL"
echo "{\"host\":\"${HOST}\",\"port\":${PORT},\"expiry\":\"${EXPIRY}\",\"days_left\":${DAYS_LEFT},\"level\":\"${LEVEL}\"}"
""",
parameters='[{"name":"HOST","desc":"도메인 또는 IP","required":true},{"name":"PORT","desc":"HTTPS 포트, 기본 443","required":false}]',
is_dangerous=False, requires_approval=False,
author="system", tags="ssl tls certificate 인증서 만료 expiry https 보안",
),
ShellScript(
script_name="quarterly_regular_check",
category="REGULAR", sub_category="QUARTERLY_AUDIT",
target_layer="ALL", os_type="LINUX",
description="분기 정기점검 종합 스크립트. 리소스·서비스·로그·SSL 항목을 순서대로 점검하고 JSON 보고서 출력.",
script_body="""#!/bin/bash
# 분기 정기점검 종합 스크립트
# 출력: 각 항목별 JSON 결과
echo "=== GUARDiA 분기 정기점검 시작 ==="
echo "점검일시: $(date '+%Y-%m-%d %H:%M:%S')"
echo ""
# 1. 리소스 현황
echo "--- [1] 시스템 리소스 ---"
echo "CPU: $(top -bn1 | grep 'Cpu(s)' | awk '{printf \"%.1f%%\", 100-$8}')"
echo "MEM: $(free -h | awk '/^Mem:/{printf \"%s / %s\", $3, $2}')"
echo "DISK: $(df -h / | awk 'NR==2{print $3\" / \"$2\" (\"$5\")\"}')"
echo ""
# 2. 서비스 상태
echo "--- [2] 서비스 상태 ---"
for svc in nginx tomcat9 postgresql; do
status=$(systemctl is-active $svc 2>/dev/null)
echo "${svc}: ${status:-not_installed}"
done
echo ""
# 3. 최근 에러 로그 확인
echo "--- [3] 최근 에러 로그 (최근 24시간) ---"
find /app/logs /var/log/nginx /var/log/postgresql -name "*.log" 2>/dev/null | while read f; do
cnt=$(grep -c -i "error\\|exception\\|critical" "$f" 2>/dev/null || echo 0)
[ "$cnt" -gt 0 ] && echo " ${f}: ${cnt}"
done
echo ""
echo "=== 정기점검 완료 ==="
""",
is_dangerous=False, requires_approval=False,
author="system", tags="regular 정기점검 quarterly 분기 종합 comprehensive audit",
),
ShellScript(
script_name="emergency_oom_response",
category="ADHOC", sub_category="OOM_RESPONSE",
target_layer="WAS", os_type="LINUX",
description="OOM(OutOfMemoryError) 긴급 대응 스크립트. 힙 덤프 생성 후 WAS 재기동 및 메모리 상태 보고.",
script_body="""#!/bin/bash
# OOM 긴급 대응 스크립트
# 주의: WAS 재기동이 포함됩니다. 승인 후 실행하세요.
WAS_SVC=${1:-tomcat9}
DUMP_DIR=${2:-/app/heap_dumps}
mkdir -p "${DUMP_DIR}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# 현재 Java 프로세스 PID 확인
JAVA_PID=$(pgrep -f tomcat | head -1)
echo "{\"step\":\"pid_check\",\"pid\":\"${JAVA_PID:-not_found}\"}"
# 힙 덤프 생성 (가능한 경우)
if [ -n "${JAVA_PID}" ] && command -v jmap &>/dev/null; then
DUMP_FILE="${DUMP_DIR}/heap_${TIMESTAMP}.hprof"
jmap -dump:format=b,file="${DUMP_FILE}" "${JAVA_PID}" 2>/dev/null
echo "{\"step\":\"heap_dump\",\"file\":\"${DUMP_FILE}\"}"
fi
# 메모리 상태 수집
echo "{\"step\":\"memory_before\",\"free_output\":\"$(free -m | grep Mem)\"}"
# WAS 재기동
sudo systemctl restart "${WAS_SVC}"
RESTART_CODE=$?
echo "{\"step\":\"restart\",\"service\":\"${WAS_SVC}\",\"result\":\"$([ $RESTART_CODE -eq 0 ] && echo SUCCESS || echo FAILED)\"}"
sleep 10
echo "{\"step\":\"memory_after\",\"free_output\":\"$(free -m | grep Mem)\"}"
""",
parameters='[{"name":"WAS_SVC","desc":"WAS 서비스명 (기본 tomcat9)","required":false},{"name":"DUMP_DIR","desc":"힙 덤프 저장 경로","required":false}]',
is_dangerous=True, requires_approval=True,
author="system", tags="oom outofmemory heap dump 긴급대응 emergency was restart 재기동",
),
]
for s in scripts:
db.add(s)
await db.commit()
async def _seed_timetable(db: AsyncSession) -> None:
"""작업 타임테이블 시드 데이터."""
from models import WorkTimetable
r = await db.execute(select(WorkTimetable))
if r.scalars().first():
return
# 기관 ID 조회
inst_r = await db.execute(select(Institution).where(Institution.inst_code == "MOF"))
mof = inst_r.scalars().first()
inst_mois_r = await db.execute(select(Institution).where(Institution.inst_code == "MOIS"))
mois = inst_mois_r.scalars().first()
if not mof:
return
now = datetime.now()
entries = [
WorkTimetable(
work_type="REGULAR_CHECK",
title="2025년 Q1 정기점검 — 기획재정부",
inst_id=mof.id if mof else None,
scheduled_at=now.replace(month=3, day=15, hour=10, minute=0),
started_at=now.replace(month=3, day=15, hour=10, minute=5),
completed_at=now.replace(month=3, day=15, hour=12, minute=30),
content="WEB/WAS/DB 서버 전체 정기점검. 리소스 현황, 서비스 상태, 에러 로그, SSL 인증서 만료일 확인",
command_or_shell="quarterly_regular_check.sh",
result="전체 정상. WAS CPU 72%, 메모리 68%. SSL 만료 D-45 (portal.mof.go.kr) — 갱신 일정 수립 권고",
result_status="SUCCESS",
assignee="engineer1",
reviewer="pm1",
note="SSL 만료 임박 건 별도 SR 접수 예정",
created_by="engineer1",
),
WorkTimetable(
work_type="SR",
title="기재부 WAS OOM 긴급 대응 — SR-20260524-BB3C4D",
inst_id=mof.id if mof else None,
sr_id="SR-20260524-BB3C4D",
scheduled_at=now.replace(hour=14, minute=0),
started_at=now.replace(hour=14, minute=5),
completed_at=now.replace(hour=15, minute=20),
content="OutOfMemoryError 발생으로 MOF-WAS-01 재기동. 힙 덤프 생성 후 원인 분석 수행",
command_or_shell="emergency_oom_response.sh tomcat9 /app/heap_dumps",
result="WAS 재기동 성공. 힙 덤프 저장: /app/heap_dumps/heap_20260524_140500.hprof. 메모리 누수 패턴 발견 — 개발팀 전달 예정",
result_status="SUCCESS",
assignee="engineer1",
reviewer="pm1",
created_by="engineer1",
),
WorkTimetable(
work_type="PM",
title="행안부 서버 패치 적용 — 2025년 5월 정기 PM",
inst_id=mois.id if mois else None,
scheduled_at=now.replace(month=5, day=20, hour=22, minute=0),
started_at=now.replace(month=5, day=20, hour=22, minute=10),
completed_at=now.replace(month=5, day=21, hour=1, minute=30),
content="MOIS WEB/WAS 서버 OS 보안 패치(CVE-2025-xxxx) 적용. 패치 후 서비스 재기동 및 정상 여부 확인",
command_or_shell="yum update -y --security && systemctl restart nginx tomcat9",
result="패치 적용 완료. nginx/tomcat9 정상 기동 확인. 패치 적용 패키지: 12건",
result_status="SUCCESS",
assignee="engineer2",
reviewer="pm1",
created_by="engineer2",
),
WorkTimetable(
work_type="ADHOC",
title="중기부 WAS 로그 수시 점검 — Connection Pool 오류",
scheduled_at=now.replace(hour=9, minute=0),
content="MSS-WAS-01 Connection pool exhausted 오류 원인 분석. 애플리케이션 로그 및 DB 세션 현황 점검",
command_or_shell="collect_target_logs.sh /app/tomcat/logs/catalina.out 500",
result="HikariCP 커넥션 풀 200/200 포화 확인. DB 세션 잠금 6건 발견. pg_terminate_backend로 잠금 해제 후 정상화",
result_status="SUCCESS",
assignee="engineer2",
created_by="engineer2",
),
WorkTimetable(
work_type="DEPLOY",
title="기재부 예산시스템 2차 추경 반영 배포",
inst_id=mof.id if mof else None,
sr_id="SR-20260524-AA1B2C",
scheduled_at=now.replace(hour=18, minute=0),
started_at=now.replace(hour=18, minute=5),
completed_at=now.replace(hour=19, minute=30),
content="2025년 2차 추경 예산시스템 class 파일 배포. MOF-WAS-01 롤링 재기동 적용",
command_or_shell="manage_service.sh WAS restart",
result="배포 완료. WAS 롤링 재기동 성공. 헬스체크 HTTP 200 확인",
result_status="SUCCESS",
assignee="engineer1",
reviewer="pm1",
created_by="engineer1",
),
WorkTimetable(
work_type="REGULAR_CHECK",
title="2025년 Q2 정기점검 예정 — 기획재정부",
inst_id=mof.id if mof else None,
scheduled_at=now.replace(month=6, day=15, hour=10, minute=0),
content="2025년 2분기 정기점검 계획. 내용: 리소스·서비스·보안 취약점·SSL·백업 상태 점검",
command_or_shell="quarterly_regular_check.sh",
result_status="PENDING",
assignee="engineer1",
reviewer="pm1",
created_by="pm1",
),
]
for e in entries:
db.add(e)
await db.commit()
async def _seed_pm_templates(db: AsyncSession) -> None:
"""정기 PM 체크리스트 기본 템플릿 시드."""
from models import PmTemplate
r = await db.execute(select(PmTemplate))
if r.scalars().first():
return
templates = [
# ── OS 공통 점검 ───────────────────────────────────────────────────────
PmTemplate(
template_name="OS 기본 점검", server_role="ALL", category="OS",
item_order=1, item_title="CPU 사용률 확인",
item_desc="평균 CPU 사용률 85% 미만 여부 확인",
check_command="top -bn1 | grep 'Cpu(s)' | awk '{print 100-$8}'",
expected_value="85% 미만", is_mandatory=True,
),
PmTemplate(
template_name="OS 기본 점검", server_role="ALL", category="OS",
item_order=2, item_title="메모리 사용률 확인",
item_desc="메모리 사용률 90% 미만 여부 확인",
check_command="free -m | awk '/^Mem:/{printf \"%.0f\", $3/$2*100}'",
expected_value="90% 미만", is_mandatory=True,
),
PmTemplate(
template_name="OS 기본 점검", server_role="ALL", category="OS",
item_order=3, item_title="디스크 사용률 확인",
item_desc="주요 파티션 사용률 85% 미만 여부 확인",
check_command="df -h | awk 'NR>1{print $5,$6}' | grep -v tmpfs",
expected_value="85% 미만", is_mandatory=True,
),
PmTemplate(
template_name="OS 기본 점검", server_role="ALL", category="OS",
item_order=4, item_title="시스템 로그 에러 확인",
item_desc="최근 7일간 /var/log/messages 에러 건수 확인",
check_command="grep -c 'error\\|critical\\|panic' /var/log/messages 2>/dev/null || echo 0",
expected_value="신규 심각 오류 없음", is_mandatory=True,
),
PmTemplate(
template_name="OS 기본 점검", server_role="ALL", category="OS",
item_order=5, item_title="OS 보안 패치 현황",
item_desc="보안 업데이트 가용 여부 확인",
check_command="yum check-update --security 2>/dev/null | tail -5",
expected_value="필수 보안 패치 적용 완료", is_mandatory=False,
),
PmTemplate(
template_name="OS 기본 점검", server_role="ALL", category="OS",
item_order=6, item_title="NTP 동기화 상태",
item_desc="시간 동기화 정상 여부 확인",
check_command="chronyc tracking 2>/dev/null || ntpstat 2>/dev/null || timedatectl",
expected_value="동기화 정상", is_mandatory=True,
),
# ── WEB 서버 점검 ──────────────────────────────────────────────────────
PmTemplate(
template_name="WEB 서버 점검", server_role="WEB", category="WEB",
item_order=1, item_title="Nginx/Apache 프로세스 확인",
item_desc="웹서버 프로세스 정상 기동 여부",
check_command="systemctl is-active nginx || systemctl is-active httpd",
expected_value="active", is_mandatory=True,
),
PmTemplate(
template_name="WEB 서버 점검", server_role="WEB", category="WEB",
item_order=2, item_title="HTTP 80/443 포트 리슨 확인",
item_desc="서비스 포트 리슨 상태 확인",
check_command="ss -tlnp | grep -E ':80|:443'",
expected_value="LISTEN 상태", is_mandatory=True,
),
PmTemplate(
template_name="WEB 서버 점검", server_role="WEB", category="WEB",
item_order=3, item_title="SSL 인증서 만료일 확인",
item_desc="SSL 인증서 잔여 유효 기간 D-30 이상 여부",
check_command="echo | openssl s_client -connect localhost:443 2>/dev/null | openssl x509 -noout -enddate",
expected_value="D-30 이상", is_mandatory=True,
),
PmTemplate(
template_name="WEB 서버 점검", server_role="WEB", category="WEB",
item_order=4, item_title="Nginx 에러 로그 확인",
item_desc="최근 에러 로그 5xx 건수",
check_command="grep -c ' 5[0-9][0-9] ' /var/log/nginx/access.log 2>/dev/null || echo 0",
expected_value="5xx 응답 없음 또는 정상 범위", is_mandatory=False,
),
# ── WAS 서버 점검 ──────────────────────────────────────────────────────
PmTemplate(
template_name="WAS 서버 점검", server_role="WAS", category="WAS",
item_order=1, item_title="Tomcat/JBoss 서비스 상태",
item_desc="WAS 프로세스 정상 기동 여부",
check_command="systemctl is-active tomcat9 || systemctl is-active jboss",
expected_value="active", is_mandatory=True,
),
PmTemplate(
template_name="WAS 서버 점검", server_role="WAS", category="WAS",
item_order=2, item_title="JVM 힙 메모리 사용률",
item_desc="JVM 힙 사용률 80% 미만 여부",
check_command="jstat -gc $(pgrep -f tomcat | head -1) 2>/dev/null | awk 'NR==2{printf \"%.0f\", ($8+$6)/$7*100}'",
expected_value="80% 미만", is_mandatory=True,
),
PmTemplate(
template_name="WAS 서버 점검", server_role="WAS", category="WAS",
item_order=3, item_title="WAS 에러 로그 확인",
item_desc="catalina.out 최근 Exception/ERROR 건수",
check_command="tail -1000 /app/tomcat/logs/catalina.out 2>/dev/null | grep -c -i 'exception\\|error'",
expected_value="신규 예외 없음", is_mandatory=True,
),
PmTemplate(
template_name="WAS 서버 점검", server_role="WAS", category="WAS",
item_order=4, item_title="힙 덤프 파일 존재 여부",
item_desc="미처리 힙 덤프(.hprof) 파일 없음 확인",
check_command="find /app /tmp -name '*.hprof' 2>/dev/null | wc -l",
expected_value="0 (없음)", is_mandatory=False,
),
PmTemplate(
template_name="WAS 서버 점검", server_role="WAS", category="WAS",
item_order=5, item_title="스레드 덤프 확인 (부하 시)",
item_desc="스레드 블로킹 여부 확인",
check_command="jstack $(pgrep -f tomcat | head -1) 2>/dev/null | grep -c BLOCKED",
expected_value="BLOCKED 스레드 없음", is_mandatory=False,
),
# ── DB 서버 점검 ────────────────────────────────────────────────────────
PmTemplate(
template_name="DB 서버 점검", server_role="DB", category="DB",
item_order=1, item_title="DB 서비스 상태 확인",
item_desc="PostgreSQL/Oracle/MySQL 서비스 기동 여부",
check_command="systemctl is-active postgresql || systemctl is-active oracle || systemctl is-active mysqld",
expected_value="active", is_mandatory=True,
),
PmTemplate(
template_name="DB 서버 점검", server_role="DB", category="DB",
item_order=2, item_title="DB 연결 수 확인",
item_desc="현재 활성 연결 수 최대값 80% 미만 여부",
check_command="psql -c 'SELECT count(*) FROM pg_stat_activity;' 2>/dev/null",
expected_value="max_connections의 80% 미만", is_mandatory=True,
),
PmTemplate(
template_name="DB 서버 점검", server_role="DB", category="DB",
item_order=3, item_title="테이블스페이스/파티션 여유 공간",
item_desc="DB 데이터 파티션 사용률 85% 미만",
check_command="df -h /data /pgdata /oradata 2>/dev/null | awk 'NR>1{print $5,$6}'",
expected_value="85% 미만", is_mandatory=True,
),
PmTemplate(
template_name="DB 서버 점검", server_role="DB", category="DB",
item_order=4, item_title="백업 최근 실행 여부 확인",
item_desc="최근 1일 이내 백업 파일 존재 여부",
check_command="find /backup /data/backup -name '*.dump' -mtime -1 2>/dev/null | wc -l",
expected_value="1 이상 (최근 백업 존재)", is_mandatory=True,
),
PmTemplate(
template_name="DB 서버 점검", server_role="DB", category="DB",
item_order=5, item_title="장기 잠금(Lock) 세션 확인",
item_desc="5분 이상 대기 중인 잠금 세션 없음",
check_command="psql -c \"SELECT count(*) FROM pg_locks l JOIN pg_stat_activity a ON l.pid=a.pid WHERE now()-a.query_start > '5 minutes';\" 2>/dev/null",
expected_value="0 (없음)", is_mandatory=False,
),
# ── 보안 공통 점검 ────────────────────────────────────────────────────
PmTemplate(
template_name="보안 점검", server_role="ALL", category="SECURITY",
item_order=1, item_title="불필요 오픈 포트 확인",
item_desc="승인되지 않은 포트 Listen 여부 확인",
check_command="ss -tlnp | grep -v '22\|80\|443\|8080\|5432\|1521'",
expected_value="승인된 포트만 오픈", is_mandatory=True,
),
PmTemplate(
template_name="보안 점검", server_role="ALL", category="SECURITY",
item_order=2, item_title="root 직접 SSH 접속 제한",
item_desc="PermitRootLogin no 설정 여부",
check_command="grep 'PermitRootLogin' /etc/ssh/sshd_config",
expected_value="PermitRootLogin no", is_mandatory=True,
),
PmTemplate(
template_name="보안 점검", server_role="ALL", category="SECURITY",
item_order=3, item_title="로그인 실패 기록 확인",
item_desc="최근 24시간 SSH 로그인 실패 50회 이상 여부",
check_command="grep 'Failed password' /var/log/secure 2>/dev/null | grep \"$(date +%b' '%e)\" | wc -l",
expected_value="50회 미만", is_mandatory=False,
),
PmTemplate(
template_name="보안 점검", server_role="ALL", category="SECURITY",
item_order=4, item_title="방화벽(firewalld) 상태",
item_desc="방화벽 서비스 활성화 여부",
check_command="systemctl is-active firewalld",
expected_value="active", is_mandatory=True,
),
]
for tmpl in templates:
db.add(tmpl)
await db.commit()
async def seed_all(db: AsyncSession) -> None:
await _seed_users(db)
await _seed_engineer_profiles(db)
await _seed_kb_documents(db)
await _seed_shell_scripts(db)
await _seed_pm_templates(db)
result = await db.execute(select(Institution))
if result.scalars().first():
await _seed_timetable(db)
return # already seeded
# ── Institutions ────────────────────────────────────────────
from datetime import date as _date
inst_data = [
{
"inst_code": "MOF", "inst_name": "기획재정부",
"org_type": "중앙행정기관", "contact_pm": "김PM",
"address": "세종특별자치시 갈매로 477", "region": "세종",
"phone": "044-215-2114",
"contract_start": _date(2025, 1, 1), "contract_end": _date(2025, 12, 31),
"sla_hours": 4,
},
{
"inst_code": "MOIS", "inst_name": "행정안전부",
"org_type": "중앙행정기관", "contact_pm": "이PM",
"address": "세종특별자치시 한누리대로 411", "region": "세종",
"phone": "044-205-3114",
"contract_start": _date(2025, 4, 1), "contract_end": _date(2026, 3, 31),
"sla_hours": 2,
},
{
"inst_code": "MSS", "inst_name": "중소벤처기업부",
"org_type": "중앙행정기관", "contact_pm": "박PM",
"address": "세종특별자치시 다솜2로 94", "region": "세종",
"phone": "044-204-7114",
"contract_start": _date(2025, 1, 1), "contract_end": _date(2025, 12, 31),
"sla_hours": 4,
},
]
institutions = []
for d in inst_data:
inst = Institution(**d)
db.add(inst)
institutions.append(inst)
await db.flush()
# ── InstContacts ─────────────────────────────────────────────
from models import InstContact
contact_data = [
# MOF 담당자
{"inst": institutions[0], "contact_name": "김재정", "dept": "정보화담당관실",
"position": "사무관", "role": "MANAGER", "email": "kjj@mof.go.kr",
"phone": "044-215-2100", "mobile": "010-1234-5678", "is_primary": True},
{"inst": institutions[0], "contact_name": "이시스템", "dept": "정보화담당관실",
"position": "주무관", "role": "ENGINEER", "email": "lss@mof.go.kr",
"phone": "044-215-2101", "mobile": "010-2345-6789"},
{"inst": institutions[0], "contact_name": "박보안", "dept": "정보보안팀",
"position": "팀장", "role": "SECURITY", "email": "pbr@mof.go.kr",
"phone": "044-215-2200", "mobile": "010-3456-7890"},
# MOIS 담당자
{"inst": institutions[1], "contact_name": "최행정", "dept": "디지털정부국",
"position": "서기관", "role": "MANAGER", "email": "chj@mois.go.kr",
"phone": "044-205-3100", "mobile": "010-4567-8901", "is_primary": True},
{"inst": institutions[1], "contact_name": "정운영", "dept": "디지털정부국",
"position": "주무관", "role": "ENGINEER", "email": "juo@mois.go.kr",
"phone": "044-205-3101", "mobile": "010-5678-9012"},
# MSS 담당자
{"inst": institutions[2], "contact_name": "한중기", "dept": "정보화기획과",
"position": "팀장", "role": "MANAGER", "email": "hjk@mss.go.kr",
"phone": "044-204-7100", "mobile": "010-6789-0123", "is_primary": True},
]
for cd in contact_data:
inst = cd.pop("inst")
contact = InstContact(inst_id=inst.id, **cd)
db.add(contact)
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()
await _seed_timetable(db)