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>
1088 lines
54 KiB
Python
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)
|