zioinfo-mail/workspace/guardia-itsm/routers/config_inventory.py
DESKTOP-TKLFCPR\ython b8faec44e0 feat(advanced): GUARDiA 고급 확장 구현 — 20 routers + 754 endpoints
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>
2026-06-02 14:33:41 +09:00

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