CMDB 자동 발견 (4개): - autodiscovery.py: SSH 네트워크 스캔 + CMDB 자동 등록 - snmp_discovery.py: SNMP v2c/v3 장비 자동 발견 - dependency_map.py: 서비스 의존성 자동 매핑 (netstat) - config_inventory.py: 서버 인벤토리 자동 수집 (SSH) NL 쿼리 엔진 (3개): - nlquery.py: Text-to-SQL (SELECT 전용, DML 차단) - op_assistant.py: Multi-turn 대화형 운영 어시스턴트 - query_history.py: 쿼리 이력·즐겨찾기·공유 구성 드리프트 (3개): - drift_detection.py: 골든 구성 vs 실제 비교·SR 자동 생성 - golden_config.py: 내장 CSAP 템플릿 + 버전 관리 - auto_remediation.py: 승인 기반 자동 교정 + 롤백 멀티클라우드 (4개): - multicloud.py: 통합 관제 (NCloud+AWS+KT) - aws_connector.py: AWS SigV4 직접 서명 연동 - cost_optimizer.py: AI 비용 최적화 권고 - cloud_migration.py: On-prem→K-Cloud 체크리스트 공공기관 특화 (6개): - narasajang.py: 나라장터 OpenAPI 연동 - public_api_hub.py: data.go.kr KISA·기상청 허브 - isp_support.py: ISP 수립 지원 + AI 보고서 - network_zone.py: 행정망/인터넷망 분리 관리 - k_cloud.py: 정부 K-Cloud 전환 자동화 - e_procurement.py: 전자조달 계약·검수·납품 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
232 lines
7.9 KiB
Python
232 lines
7.9 KiB
Python
"""
|
|
서비스 의존성 자동 매핑
|
|
|
|
SSH 경유 netstat/ss 분석으로 서비스 간 upstream/downstream 의존성을 자동 탐지.
|
|
|
|
엔드포인트:
|
|
POST /api/depmap/discover/{server_id} — 단일 서버 의존성 탐지
|
|
POST /api/depmap/discover-all — 전체 서버 의존성 탐지
|
|
GET /api/depmap/ — 의존성 맵 조회
|
|
GET /api/depmap/impact/{ci_id} — 특정 CI 영향 범위 분석
|
|
DELETE /api/depmap/{dep_id} — 의존성 수동 삭제
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
from datetime import datetime
|
|
|
|
import paramiko
|
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
|
from sqlalchemy import select, or_
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from core.auth import get_current_user, require_admin_role
|
|
from core.ssh_exec import _decrypt_password as decrypt_password
|
|
from database import get_db
|
|
from models import User, Server, ServiceDependency
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/depmap", tags=["서비스 의존성 맵"])
|
|
|
|
|
|
async def _ssh_run(server: Server, cmd: str) -> str:
|
|
try:
|
|
pw = decrypt_password(server.os_pw_enc)
|
|
ssh = paramiko.SSHClient()
|
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
ssh.connect(server.ip_addr, username=server.ssh_user, password=pw, timeout=10)
|
|
_, stdout, _ = ssh.exec_command(cmd, timeout=15)
|
|
result = stdout.read().decode('utf-8', 'replace').strip()
|
|
ssh.close()
|
|
return result
|
|
except Exception as e:
|
|
logger.warning(f"SSH 실패 ({server.ip_addr}): {e}")
|
|
return ""
|
|
|
|
|
|
async def _discover_connections(server: Server) -> list[dict]:
|
|
"""netstat/ss로 ESTABLISHED 연결 분석 → 의존성 추출."""
|
|
output = await _ssh_run(
|
|
server,
|
|
"ss -tnp state established 2>/dev/null | awk 'NR>1{print $4,$5}' || "
|
|
"netstat -tnp 2>/dev/null | grep ESTABLISHED | awk '{print $4,$5}'"
|
|
)
|
|
connections = []
|
|
seen = set()
|
|
for line in output.splitlines():
|
|
parts = line.split()
|
|
if len(parts) < 2:
|
|
continue
|
|
local, remote = parts[0], parts[1]
|
|
# 외부 IP:포트 추출
|
|
m = re.match(r'(\d+\.\d+\.\d+\.\d+):(\d+)$', remote)
|
|
if not m:
|
|
continue
|
|
remote_ip, remote_port = m.group(1), int(m.group(2))
|
|
if remote_ip in ("127.0.0.1", "0.0.0.0"):
|
|
continue
|
|
key = f"{remote_ip}:{remote_port}"
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
# 포트 → 서비스 유형 추정
|
|
service_map = {
|
|
3306: "MySQL", 5432: "PostgreSQL", 6379: "Redis",
|
|
27017: "MongoDB", 9200: "Elasticsearch",
|
|
80: "HTTP", 443: "HTTPS", 8080: "HTTP-Alt",
|
|
2181: "Zookeeper", 9092: "Kafka", 5672: "RabbitMQ",
|
|
}
|
|
dep_type = service_map.get(remote_port, f"TCP:{remote_port}")
|
|
connections.append({
|
|
"remote_ip": remote_ip, "remote_port": remote_port,
|
|
"dependency_type": dep_type, "protocol": "TCP",
|
|
})
|
|
return connections
|
|
|
|
|
|
async def _do_discover(server_id: int, db: AsyncSession):
|
|
"""단일 서버 의존성 탐지 (백그라운드)."""
|
|
srv_row = await db.execute(select(Server).where(Server.id == server_id))
|
|
server = srv_row.scalar_one_or_none()
|
|
if not server:
|
|
return
|
|
|
|
connections = await _discover_connections(server)
|
|
new_deps = 0
|
|
for conn in connections:
|
|
# 원격 IP가 CMDB에 있는지 확인
|
|
remote_srv_row = await db.execute(
|
|
select(Server).where(Server.ip_addr == conn["remote_ip"])
|
|
)
|
|
remote_server = remote_srv_row.scalar_one_or_none()
|
|
if not remote_server:
|
|
continue # CMDB에 없는 서버는 스킵
|
|
|
|
# 중복 체크
|
|
existing = await db.execute(
|
|
select(ServiceDependency).where(
|
|
ServiceDependency.upstream_ci_id == server_id,
|
|
ServiceDependency.downstream_ci_id == remote_server.id,
|
|
ServiceDependency.port == conn["remote_port"],
|
|
)
|
|
)
|
|
if existing.scalar_one_or_none():
|
|
continue
|
|
|
|
dep = ServiceDependency(
|
|
upstream_ci_id=server_id,
|
|
downstream_ci_id=remote_server.id,
|
|
dependency_type=conn["dependency_type"],
|
|
port=conn["remote_port"],
|
|
protocol=conn["protocol"],
|
|
discovered_at=datetime.utcnow(),
|
|
)
|
|
db.add(dep)
|
|
new_deps += 1
|
|
|
|
await db.commit()
|
|
logger.info(f"서버 {server_id} 의존성 {new_deps}개 발견")
|
|
|
|
|
|
@router.post("/discover/{server_id}")
|
|
async def discover_server_deps(
|
|
server_id: int,
|
|
background_tasks: BackgroundTasks,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
background_tasks.add_task(_do_discover, server_id, db)
|
|
return {"ok": True, "server_id": server_id, "queued": True}
|
|
|
|
|
|
@router.post("/discover-all")
|
|
async def discover_all_deps(
|
|
background_tasks: BackgroundTasks,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_admin_role),
|
|
):
|
|
rows = await db.execute(select(Server).limit(100))
|
|
servers = rows.scalars().all()
|
|
for s in servers:
|
|
background_tasks.add_task(_do_discover, s.id, db)
|
|
return {"ok": True, "queued": len(servers)}
|
|
|
|
|
|
@router.get("/")
|
|
async def get_dependency_map(
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
rows = await db.execute(
|
|
select(
|
|
ServiceDependency,
|
|
Server.hostname.label("up_name"), Server.ip_addr.label("up_ip"),
|
|
).join(Server, ServiceDependency.upstream_ci_id == Server.id)
|
|
.limit(500)
|
|
)
|
|
deps = rows.all()
|
|
return [
|
|
{
|
|
"id": d.ServiceDependency.id,
|
|
"upstream": {"id": d.ServiceDependency.upstream_ci_id, "name": d.up_name, "ip": d.up_ip},
|
|
"downstream_id": d.ServiceDependency.downstream_ci_id,
|
|
"type": d.ServiceDependency.dependency_type,
|
|
"port": d.ServiceDependency.port,
|
|
"discovered_at": d.ServiceDependency.discovered_at,
|
|
}
|
|
for d in deps
|
|
]
|
|
|
|
|
|
@router.get("/impact/{ci_id}")
|
|
async def get_impact_analysis(
|
|
ci_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
"""특정 CI 장애 시 영향 범위 분석 (upstream 서비스 = 영향받는 서비스)."""
|
|
# ci_id에 의존하는 서비스들 (ci_id가 downstream인 경우)
|
|
rows = await db.execute(
|
|
select(ServiceDependency, Server.hostname, Server.ip_addr).join(
|
|
Server, ServiceDependency.upstream_ci_id == Server.id
|
|
).where(ServiceDependency.downstream_ci_id == ci_id)
|
|
)
|
|
impacted = rows.all()
|
|
|
|
# ci_id가 의존하는 서비스들 (ci_id가 upstream인 경우)
|
|
dep_rows = await db.execute(
|
|
select(ServiceDependency, Server.hostname, Server.ip_addr).join(
|
|
Server, ServiceDependency.downstream_ci_id == Server.id
|
|
).where(ServiceDependency.upstream_ci_id == ci_id)
|
|
)
|
|
dependencies = dep_rows.all()
|
|
|
|
return {
|
|
"ci_id": ci_id,
|
|
"impacted_services": [
|
|
{"server": r.hostname, "ip": r.ip_addr, "depends_via": r.ServiceDependency.dependency_type}
|
|
for r in impacted
|
|
],
|
|
"depends_on": [
|
|
{"server": r.hostname, "ip": r.ip_addr, "type": r.ServiceDependency.dependency_type}
|
|
for r in dependencies
|
|
],
|
|
"blast_radius": len(impacted),
|
|
}
|
|
|
|
|
|
@router.delete("/{dep_id}")
|
|
async def delete_dependency(
|
|
dep_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_admin_role),
|
|
):
|
|
row = await db.execute(select(ServiceDependency).where(ServiceDependency.id == dep_id))
|
|
dep = row.scalar_one_or_none()
|
|
if not dep:
|
|
raise HTTPException(404)
|
|
await db.delete(dep)
|
|
await db.commit()
|
|
return {"ok": True}
|