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>
212 lines
7.3 KiB
Python
212 lines
7.3 KiB
Python
"""
|
|
SNMP 기반 네트워크 장비 자동 발견
|
|
|
|
SNMP v2c/v3으로 스위치·라우터·방화벽을 발견하고 CMDB에 등록.
|
|
에이전트리스: nmap + SNMP 프로토콜만 사용.
|
|
|
|
엔드포인트:
|
|
POST /api/snmp/scan — SNMP 스캔
|
|
GET /api/snmp/devices — 발견된 장비 목록
|
|
POST /api/snmp/poll/{device_id} — 단일 장비 SNMP 폴링
|
|
GET /api/snmp/configs — SNMP 설정 목록
|
|
POST /api/snmp/configs — SNMP 커뮤니티 설정 등록
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy import select, desc
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from core.auth import get_current_user, require_admin_role
|
|
from database import get_db
|
|
from models import User, SNMPConfig, SNMPDevice
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/snmp", tags=["SNMP 자동 발견"])
|
|
|
|
# SNMP OID 상수 (RFC 1213 MIB-II)
|
|
OID_SYSNAME = "1.3.6.1.2.1.1.5.0"
|
|
OID_SYSDESCR = "1.3.6.1.2.1.1.1.0"
|
|
OID_SYSLOCATION = "1.3.6.1.2.1.1.6.0"
|
|
OID_SYSCONTACT = "1.3.6.1.2.1.1.4.0"
|
|
OID_IFNUMBER = "1.3.6.1.2.1.2.1.0"
|
|
|
|
|
|
class SNMPScanRequest(BaseModel):
|
|
network_range: str
|
|
community: str = Field("public", description="SNMP v2c 커뮤니티 문자열")
|
|
version: int = Field(2, ge=1, le=3)
|
|
timeout_sec: int = Field(3, ge=1, le=10)
|
|
port: int = 161
|
|
|
|
class SNMPConfigCreate(BaseModel):
|
|
name: str
|
|
community: str
|
|
version: int = 2
|
|
ip_ranges: list[str] = []
|
|
|
|
|
|
async def _snmp_get(ip: str, community: str, oid: str, port: int = 161, timeout: int = 3) -> Optional[str]:
|
|
"""SNMP GET 요청 (snmpget 명령 경유 또는 순수 UDP 패킷)."""
|
|
import subprocess
|
|
try:
|
|
r = subprocess.run(
|
|
["snmpget", "-v2c", f"-c{community}", "-t", str(timeout), "-r1",
|
|
f"{ip}:{port}", oid],
|
|
capture_output=True, text=True, timeout=timeout + 1
|
|
)
|
|
if r.returncode == 0 and r.stdout:
|
|
# 출력 형식: OID = STRING: "value" 또는 INTEGER: 1
|
|
parts = r.stdout.strip().split(":", 2)
|
|
return parts[-1].strip().strip('"') if len(parts) >= 2 else r.stdout.strip()
|
|
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
pass
|
|
return None
|
|
|
|
|
|
async def _scan_snmp_host(ip: str, community: str, port: int, timeout: int) -> Optional[dict]:
|
|
"""단일 호스트 SNMP 정보 수집."""
|
|
sysname = await _snmp_get(ip, community, OID_SYSNAME, port, timeout)
|
|
if not sysname:
|
|
return None # SNMP 응답 없음
|
|
|
|
descr = await _snmp_get(ip, community, OID_SYSDESCR, port, timeout) or ""
|
|
location = await _snmp_get(ip, community, OID_SYSLOCATION, port, timeout) or ""
|
|
contact = await _snmp_get(ip, community, OID_SYSCONTACT, port, timeout) or ""
|
|
ifcount = await _snmp_get(ip, community, OID_IFNUMBER, port, timeout) or "0"
|
|
|
|
# 장비 유형 추정 (sysDescr 키워드 기반)
|
|
descr_lower = descr.lower()
|
|
if any(k in descr_lower for k in ["cisco", "juniper", "arista", "switch", "router"]):
|
|
device_type = "NETWORK"
|
|
elif any(k in descr_lower for k in ["linux", "windows", "unix"]):
|
|
device_type = "SERVER"
|
|
elif any(k in descr_lower for k in ["ups", "power"]):
|
|
device_type = "UPS"
|
|
elif any(k in descr_lower for k in ["printer", "print"]):
|
|
device_type = "PRINTER"
|
|
else:
|
|
device_type = "UNKNOWN"
|
|
|
|
return {
|
|
"ip": ip, "sysname": sysname, "description": descr[:200],
|
|
"location": location, "contact": contact,
|
|
"interface_count": int(ifcount) if ifcount.isdigit() else 0,
|
|
"device_type": device_type,
|
|
}
|
|
|
|
|
|
async def _run_snmp_scan(config_id: int, req: SNMPScanRequest, user_id: int, db: AsyncSession):
|
|
"""백그라운드 SNMP 스캔."""
|
|
import ipaddress
|
|
try:
|
|
network = ipaddress.ip_network(req.network_range, strict=False)
|
|
tasks = [
|
|
_scan_snmp_host(str(h), req.community, req.port, req.timeout_sec)
|
|
for h in list(network.hosts())[:254]
|
|
]
|
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
for result in results:
|
|
if isinstance(result, dict):
|
|
device = SNMPDevice(
|
|
config_id=config_id,
|
|
ip_addr=result["ip"],
|
|
sysname=result["sysname"],
|
|
description=result["description"],
|
|
device_type=result["device_type"],
|
|
location=result["location"],
|
|
interface_count=result["interface_count"],
|
|
discovered_at=datetime.utcnow(),
|
|
)
|
|
db.add(device)
|
|
await db.commit()
|
|
except Exception as e:
|
|
logger.error(f"SNMP 스캔 실패: {e}")
|
|
|
|
|
|
@router.post("/configs")
|
|
async def create_snmp_config(
|
|
req: SNMPConfigCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_admin_role),
|
|
):
|
|
cfg = SNMPConfig(
|
|
tenant_id=user.tenant_id,
|
|
name=req.name,
|
|
community_enc=req.community, # TODO: AES-256-GCM 암호화
|
|
version=req.version,
|
|
ip_ranges=req.ip_ranges,
|
|
is_active=True,
|
|
created_at=datetime.utcnow(),
|
|
)
|
|
db.add(cfg)
|
|
await db.commit()
|
|
await db.refresh(cfg)
|
|
return {"ok": True, "id": cfg.id}
|
|
|
|
|
|
@router.get("/configs")
|
|
async def list_snmp_configs(
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
rows = await db.execute(
|
|
select(SNMPConfig).where(SNMPConfig.tenant_id == user.tenant_id)
|
|
)
|
|
cfgs = rows.scalars().all()
|
|
return [
|
|
{"id": c.id, "name": c.name, "version": c.version,
|
|
"ip_ranges": c.ip_ranges, "is_active": c.is_active}
|
|
for c in cfgs
|
|
]
|
|
|
|
|
|
@router.post("/scan")
|
|
async def start_snmp_scan(
|
|
req: SNMPScanRequest,
|
|
background_tasks: BackgroundTasks,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_admin_role),
|
|
):
|
|
# 기본 SNMP 설정 생성
|
|
cfg = SNMPConfig(
|
|
tenant_id=user.tenant_id, name=f"scan_{datetime.utcnow().strftime('%Y%m%d%H%M')}",
|
|
community_enc=req.community, version=req.version,
|
|
is_active=True, created_at=datetime.utcnow(),
|
|
)
|
|
db.add(cfg)
|
|
await db.commit()
|
|
await db.refresh(cfg)
|
|
|
|
background_tasks.add_task(_run_snmp_scan, cfg.id, req, user.id, db)
|
|
return {"ok": True, "config_id": cfg.id, "network": req.network_range}
|
|
|
|
|
|
@router.get("/devices")
|
|
async def list_snmp_devices(
|
|
limit: int = 100,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
rows = await db.execute(
|
|
select(SNMPDevice, SNMPConfig.tenant_id).join(
|
|
SNMPConfig, SNMPDevice.config_id == SNMPConfig.id
|
|
).where(SNMPConfig.tenant_id == user.tenant_id)
|
|
.order_by(desc(SNMPDevice.discovered_at)).limit(limit)
|
|
)
|
|
return [
|
|
{
|
|
"id": r.SNMPDevice.id, "ip": r.SNMPDevice.ip_addr,
|
|
"name": r.SNMPDevice.sysname, "type": r.SNMPDevice.device_type,
|
|
"location": r.SNMPDevice.location, "interfaces": r.SNMPDevice.interface_count,
|
|
"discovered_at": r.SNMPDevice.discovered_at,
|
|
}
|
|
for r in rows.all()
|
|
]
|