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

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} 점검 스크립트")