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>
173 lines
6.3 KiB
Python
173 lines
6.3 KiB
Python
"""
|
|
SSH 실행 API — SM 스크립트 원격 실행 + 임시 명령어 실행.
|
|
|
|
보안:
|
|
- ADMIN / PM / ENGINEER 만 접근 가능
|
|
- 명령어 안전성 검증 (core/ssh_exec.py)
|
|
- 서버 IP/계정/비밀번호 응답에 미포함
|
|
- 모든 실행 감사 로그 기록
|
|
"""
|
|
import glob
|
|
import os
|
|
from typing import List, Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from core.auth import get_current_user
|
|
from core.ssh_exec import exec_command, exec_script
|
|
from database import get_db
|
|
from models import User, UserRole
|
|
|
|
router = APIRouter(prefix="/api/ssh", tags=["ssh"])
|
|
|
|
# SM 스크립트 루트
|
|
_SCRIPTS_ROOT = os.path.realpath(
|
|
os.path.join(os.path.dirname(__file__), "..", "scripts", "sm")
|
|
)
|
|
|
|
# ── 스키마 ────────────────────────────────────────────────────────────────────
|
|
|
|
class SSHExecRequest(BaseModel):
|
|
server_name: str
|
|
command: str
|
|
timeout: int = 120
|
|
sr_id: Optional[str] = None
|
|
|
|
|
|
class SSHScriptRequest(BaseModel):
|
|
server_name: str
|
|
script_key: str # 예: "tomcat", "system", "postgresql"
|
|
env_vars: Optional[dict] = None
|
|
timeout: int = 300
|
|
sr_id: Optional[str] = None
|
|
|
|
|
|
class SSHResultOut(BaseModel):
|
|
success: bool
|
|
stdout: str
|
|
stderr: str
|
|
exit_code: int
|
|
elapsed: float
|
|
error: str
|
|
|
|
|
|
# 허용된 역할
|
|
def _require_ops(current_user: User = Depends(get_current_user)) -> User:
|
|
if current_user.role not in (UserRole.ADMIN, UserRole.PM, UserRole.ENGINEER):
|
|
raise HTTPException(403, "SSH 실행 권한이 없습니다.")
|
|
return current_user
|
|
|
|
|
|
# SM 스크립트 키→경로 매핑
|
|
def _script_map() -> dict[str, str]:
|
|
mapping: dict[str, str] = {}
|
|
for sh in glob.glob(os.path.join(_SCRIPTS_ROOT, "**", "*.sh"), recursive=True):
|
|
key = os.path.splitext(os.path.basename(sh))[0]
|
|
# 단축 키 생성 (예: was_tomcat_sm → tomcat)
|
|
short = key.replace("_sm", "").replace("web_", "").replace("was_", "") \
|
|
.replace("db_", "").replace("search_", "").replace("agent_", "")
|
|
mapping[key] = sh
|
|
mapping[short] = sh
|
|
return mapping
|
|
|
|
|
|
# ── 엔드포인트 ────────────────────────────────────────────────────────────────
|
|
|
|
@router.get("/scripts", summary="실행 가능한 SM 스크립트 목록")
|
|
async def list_sm_scripts(
|
|
_u: User = Depends(_require_ops),
|
|
):
|
|
"""SM 스크립트 목록 반환 (경로 비포함)."""
|
|
scripts = []
|
|
for sh in sorted(glob.glob(os.path.join(_SCRIPTS_ROOT, "**", "*.sh"), recursive=True)):
|
|
rel = os.path.relpath(sh, _SCRIPTS_ROOT)
|
|
name = os.path.splitext(os.path.basename(sh))[0]
|
|
category = rel.split(os.sep)[0]
|
|
scripts.append({
|
|
"key": name,
|
|
"category": category,
|
|
"description": _script_description(name),
|
|
})
|
|
return {"scripts": scripts, "total": len(scripts)}
|
|
|
|
|
|
@router.post("/exec", response_model=SSHResultOut, summary="서버 SSH 명령 실행")
|
|
async def ssh_exec(
|
|
payload: SSHExecRequest,
|
|
current_user: User = Depends(_require_ops),
|
|
):
|
|
"""
|
|
지정 서버에 SSH 접속 후 임시 명령어 실행.
|
|
위험 패턴은 자동 차단됩니다.
|
|
"""
|
|
result = await exec_command(
|
|
server_name=payload.server_name,
|
|
command=payload.command,
|
|
timeout=payload.timeout,
|
|
actor=current_user.username,
|
|
sr_id=payload.sr_id,
|
|
)
|
|
return SSHResultOut(**result.to_dict())
|
|
|
|
|
|
@router.post("/script", response_model=SSHResultOut, summary="SM 스크립트 원격 실행")
|
|
async def ssh_run_script(
|
|
payload: SSHScriptRequest,
|
|
current_user: User = Depends(_require_ops),
|
|
):
|
|
"""
|
|
지정 서버에 SM 스크립트를 전송·실행하고 결과를 반환합니다.
|
|
script_key 예시: system, tomcat, postgresql, ping, log_analysis
|
|
"""
|
|
sm_map = _script_map()
|
|
script_path = sm_map.get(payload.script_key)
|
|
if not script_path:
|
|
raise HTTPException(
|
|
404,
|
|
f"스크립트를 찾을 수 없습니다: {payload.script_key}. "
|
|
f"GET /api/ssh/scripts 에서 목록 확인"
|
|
)
|
|
|
|
result = await exec_script(
|
|
server_name=payload.server_name,
|
|
script_path=script_path,
|
|
env_vars=payload.env_vars,
|
|
timeout=payload.timeout,
|
|
actor=current_user.username,
|
|
sr_id=payload.sr_id,
|
|
)
|
|
return SSHResultOut(**result.to_dict())
|
|
|
|
|
|
# ── 스크립트 설명 헬퍼 ────────────────────────────────────────────────────────
|
|
|
|
_DESC_MAP = {
|
|
"system_health": "시스템 리소스 종합 점검 (CPU/메모리/디스크/inode/OOM)",
|
|
"sm_full_check": "전체 SM 점검 오케스트레이터",
|
|
"web_apache_sm": "Apache HTTP Server 점검",
|
|
"web_nginx_sm": "Nginx 점검",
|
|
"web_webtob_sm": "WebtoB (TMAX) 점검",
|
|
"was_tomcat_sm": "Apache Tomcat WAS 점검",
|
|
"was_jboss_sm": "JBoss / WildFly WAS 점검",
|
|
"was_jeus_sm": "JEUS (TmaxSoft) WAS 점검",
|
|
"was_weblogic_sm": "Oracle WebLogic Server 점검",
|
|
"db_postgresql_sm": "PostgreSQL DB 점검",
|
|
"db_oracle_sm": "Oracle DB 점검",
|
|
"db_mysql_sm": "MySQL / MariaDB 점검",
|
|
"db_tibero_sm": "Tibero (TmaxSoft) DB 점검",
|
|
"esb_check": "ESB/MQ (ActiveMQ/IBM MQ/WSO2) 점검",
|
|
"search_elasticsearch_sm": "Elasticsearch 점검",
|
|
"search_solr_sm": "Apache Solr 점검",
|
|
"agent_pinpoint_sm": "Pinpoint APM 점검",
|
|
"agent_scouter_sm": "Scouter APM 점검",
|
|
"crontab_sm": "Crontab 등록 현황 점검",
|
|
"ping_test": "네트워크 연결 테스트 (ICMP/TCP)",
|
|
"log_analysis": "서버 로그 분석 (ERROR/WARN/OOM 집계)",
|
|
}
|
|
|
|
|
|
def _script_description(name: str) -> str:
|
|
return _DESC_MAP.get(name, f"{name} 점검 스크립트")
|