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>
208 lines
8.0 KiB
Python
208 lines
8.0 KiB
Python
"""
|
|
서버 구성 인벤토리 자동 수집 — SSH 에이전트리스
|
|
|
|
서버에 설치된 소프트웨어·버전·설정 파일을 SSH로 자동 수집하여 CMDB를 보완.
|
|
|
|
엔드포인트:
|
|
POST /api/inventory/collect/{server_id} — 단일 서버 인벤토리 수집
|
|
POST /api/inventory/collect-all — 전체 서버 일괄 수집
|
|
GET /api/inventory/{server_id} — 서버 인벤토리 조회
|
|
GET /api/inventory/diff/{server_id} — 이전 수집과 변경사항 비교
|
|
GET /api/inventory/software — 소프트웨어 설치 현황 (전체)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
import paramiko
|
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
|
from sqlalchemy import select, desc
|
|
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, ServerInventory
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/inventory", tags=["서버 인벤토리"])
|
|
|
|
INVENTORY_COMMANDS = {
|
|
"os_info": "cat /etc/os-release 2>/dev/null | head -5",
|
|
"kernel": "uname -r",
|
|
"hostname": "hostname -f 2>/dev/null || hostname",
|
|
"uptime": "uptime -p 2>/dev/null || uptime",
|
|
"cpu_model": "grep 'model name' /proc/cpuinfo 2>/dev/null | head -1 | cut -d: -f2 | xargs",
|
|
"cpu_cores": "nproc",
|
|
"memory_total": "free -m | awk '/^Mem:/{print $2}'",
|
|
"disk_total": "df -BG --total 2>/dev/null | tail -1 | awk '{print $2}'",
|
|
"disk_used": "df -BG --total 2>/dev/null | tail -1 | awk '{print $3}'",
|
|
"open_ports": "ss -tlnp 2>/dev/null | awk 'NR>1{print $4}' | grep -oE '[0-9]+$' | sort -n | uniq | tr '\n' ','",
|
|
"running_svcs": "systemctl list-units --type=service --state=running 2>/dev/null | grep '.service' | awk '{print $1}' | tr '\n' ','",
|
|
"java_ver": "java -version 2>&1 | head -1",
|
|
"python_ver": "python3 --version 2>/dev/null",
|
|
"node_ver": "node --version 2>/dev/null || echo none",
|
|
"nginx_ver": "nginx -v 2>&1 || echo none",
|
|
"tomcat_ver": "find /opt /usr /home -name 'catalina.sh' 2>/dev/null | head -1 | xargs -I{} sh -c 'sh {} version 2>&1 | grep -i version | head -1' || echo none",
|
|
"mysql_ver": "mysql --version 2>/dev/null || echo none",
|
|
"postgres_ver": "psql --version 2>/dev/null || echo none",
|
|
"installed_pkgs":"dpkg -l 2>/dev/null | grep '^ii' | awk '{print $2\"=\"$3}' | head -50 || rpm -qa --queryformat '%{NAME}=%{VERSION}\n' 2>/dev/null | head -50",
|
|
"crontabs": "crontab -l 2>/dev/null | grep -v '^#' | grep -v '^$' | head -10",
|
|
"users": "getent passwd 2>/dev/null | awk -F: '$3>=1000 && $3<65534{print $1}' | tr '\n' ','",
|
|
"sudoers": "grep -r ALL /etc/sudoers /etc/sudoers.d/ 2>/dev/null | grep -v '^#' | head -5",
|
|
"last_updates": "tail -5 /var/log/dpkg.log 2>/dev/null || tail -5 /var/log/yum.log 2>/dev/null || echo none",
|
|
}
|
|
|
|
|
|
async def _collect_inventory(server: Server) -> dict:
|
|
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)
|
|
results = {}
|
|
for key, cmd in INVENTORY_COMMANDS.items():
|
|
try:
|
|
_, stdout, _ = ssh.exec_command(cmd, timeout=8)
|
|
results[key] = stdout.read().decode('utf-8', 'replace').strip()[:500]
|
|
except Exception:
|
|
results[key] = ""
|
|
ssh.close()
|
|
return results
|
|
except Exception as e:
|
|
logger.warning(f"인벤토리 수집 실패 ({server.ip_addr}): {e}")
|
|
return {}
|
|
|
|
|
|
async def _do_collect(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
|
|
|
|
data = await _collect_inventory(server)
|
|
if not data:
|
|
return
|
|
|
|
inv = ServerInventory(
|
|
server_id=server_id,
|
|
data=data,
|
|
collected_at=datetime.utcnow(),
|
|
)
|
|
db.add(inv)
|
|
# 서버 정보 업데이트
|
|
if data.get("cpu_cores"):
|
|
try: server.cpu_cores = int(data["cpu_cores"])
|
|
except ValueError: pass
|
|
if data.get("memory_total"):
|
|
try: server.memory_mb = int(data["memory_total"])
|
|
except ValueError: pass
|
|
await db.commit()
|
|
|
|
|
|
@router.post("/collect/{server_id}")
|
|
async def collect_server_inventory(
|
|
server_id: int,
|
|
background_tasks: BackgroundTasks,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
background_tasks.add_task(_do_collect, server_id, db)
|
|
return {"ok": True, "server_id": server_id, "queued": True}
|
|
|
|
|
|
@router.post("/collect-all")
|
|
async def collect_all_inventory(
|
|
background_tasks: BackgroundTasks,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_admin_role),
|
|
):
|
|
rows = await db.execute(select(Server).limit(200))
|
|
servers = rows.scalars().all()
|
|
for s in servers:
|
|
background_tasks.add_task(_do_collect, s.id, db)
|
|
return {"ok": True, "queued": len(servers)}
|
|
|
|
|
|
@router.get("/{server_id}")
|
|
async def get_inventory(
|
|
server_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
row = await db.execute(
|
|
select(ServerInventory).where(ServerInventory.server_id == server_id)
|
|
.order_by(desc(ServerInventory.collected_at)).limit(1)
|
|
)
|
|
inv = row.scalar_one_or_none()
|
|
if not inv:
|
|
raise HTTPException(404, "인벤토리 없음 — 먼저 수집하세요")
|
|
return {"server_id": server_id, "data": inv.data, "collected_at": inv.collected_at}
|
|
|
|
|
|
@router.get("/diff/{server_id}")
|
|
async def get_inventory_diff(
|
|
server_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
"""최근 2개 인벤토리 비교 — 변경사항 반환."""
|
|
rows = await db.execute(
|
|
select(ServerInventory).where(ServerInventory.server_id == server_id)
|
|
.order_by(desc(ServerInventory.collected_at)).limit(2)
|
|
)
|
|
invs = rows.scalars().all()
|
|
if len(invs) < 2:
|
|
return {"server_id": server_id, "diff": {}, "message": "비교할 이전 데이터 없음"}
|
|
|
|
current = invs[0].data or {}
|
|
previous = invs[1].data or {}
|
|
diff = {}
|
|
for key in set(list(current.keys()) + list(previous.keys())):
|
|
c_val = current.get(key, "")
|
|
p_val = previous.get(key, "")
|
|
if c_val != p_val:
|
|
diff[key] = {"before": p_val[:200], "after": c_val[:200]}
|
|
|
|
return {
|
|
"server_id": server_id,
|
|
"current_at": invs[0].collected_at,
|
|
"previous_at": invs[1].collected_at,
|
|
"changed_items": len(diff),
|
|
"diff": diff,
|
|
}
|
|
|
|
|
|
@router.get("/software")
|
|
async def software_inventory(
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
"""전체 서버 소프트웨어 현황 — Java/Python/Nginx 버전 집계."""
|
|
rows = await db.execute(
|
|
select(ServerInventory, Server.hostname).join(
|
|
Server, ServerInventory.server_id == Server.id
|
|
).order_by(desc(ServerInventory.collected_at))
|
|
)
|
|
results = []
|
|
seen_servers = set()
|
|
for row in rows.all():
|
|
if row.ServerInventory.server_id in seen_servers:
|
|
continue
|
|
seen_servers.add(row.ServerInventory.server_id)
|
|
data = row.ServerInventory.data or {}
|
|
results.append({
|
|
"server": row.hostname,
|
|
"java": data.get("java_ver", "none"),
|
|
"python": data.get("python_ver", "none"),
|
|
"nginx": data.get("nginx_ver", "none"),
|
|
"node": data.get("node_ver", "none"),
|
|
"mysql": data.get("mysql_ver", "none"),
|
|
"postgres": data.get("postgres_ver", "none"),
|
|
"collected_at": row.ServerInventory.collected_at,
|
|
})
|
|
return results
|