2026-05-27 13:50:27 +07:00
|
|
|
|
import subprocess
|
|
|
|
|
|
import platform
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# SERVICE LAYER: Business Logic cho Module Monitor Config
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# Luồng đi tổng thể của module này:
|
|
|
|
|
|
#
|
|
|
|
|
|
# ┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────┐
|
|
|
|
|
|
# │ Controller│ ──→ │ Service │ ──→ │ Repository │ ──→ │ Database │
|
|
|
|
|
|
# └──────────┘ └──────────┘ └──────────────┘ └──────────┘
|
|
|
|
|
|
# │
|
|
|
|
|
|
# ├──→ Device Repository (kiểm tra thiết bị tồn tại)
|
|
|
|
|
|
# ├──→ Scheduler (cập nhật job giám sát khi config thay đổi)
|
|
|
|
|
|
# └──→ ICMP/SNMP Test (kiểm tra kết nối trực tiếp)
|
|
|
|
|
|
#
|
|
|
|
|
|
# Service chứa 3 chức năng chính:
|
|
|
|
|
|
# 1. Lấy cấu hình giám sát (GET)
|
|
|
|
|
|
# 2. Cập nhật cấu hình giám sát (PUT) + reschedule job
|
|
|
|
|
|
# 3. Test kết nối trực tiếp (POST /test) — Ping hoặc SNMP
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
|
|
|
|
|
|
from modules.monitor_config.repository import (
|
|
|
|
|
|
find_monitor_config_by_device_id,
|
2026-05-29 11:10:51 +07:00
|
|
|
|
update_monitor_config_db,
|
|
|
|
|
|
insert_default_monitor_config_db
|
2026-05-27 13:50:27 +07:00
|
|
|
|
)
|
|
|
|
|
|
from modules.monitor_config.exceptions import MonitorConfigNotFoundException
|
|
|
|
|
|
from modules.device.repository import find_device_by_id
|
|
|
|
|
|
from modules.device.exceptions import DeviceNotFoundException
|
|
|
|
|
|
from scheduler.scheduler import reschedule_device_monitoring_job
|
|
|
|
|
|
|
2026-05-29 11:10:51 +07:00
|
|
|
|
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# CREATE DEFAULT: Tạo cấu hình giám sát mặc định (Service Layer)
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
def create_default_monitor_config_service(device_id, cursor=None):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Tạo cấu hình giám sát mặc định cho thiết bị mới.
|
|
|
|
|
|
"""
|
|
|
|
|
|
return insert_default_monitor_config_db(device_id, cursor)
|
|
|
|
|
|
|
2026-05-27 13:50:27 +07:00
|
|
|
|
# ============================================
|
|
|
|
|
|
# Dynamic Import: icmplib và pysnmp
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# Tại sao cần Dynamic Import?
|
|
|
|
|
|
# - icmplib cần quyền root/sudo trên một số hệ điều hành để gửi ICMP raw socket
|
|
|
|
|
|
# - pysnmp có thể chưa được cài đặt trên máy phát triển
|
|
|
|
|
|
# - Nếu import thất bại → app vẫn chạy được, chỉ fallback sang cách khác
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
try:
|
|
|
|
|
|
from icmplib import ping as icmp_ping
|
|
|
|
|
|
HAS_ICMPLIB = True
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
HAS_ICMPLIB = False
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
from pysnmp.hlapi import (
|
|
|
|
|
|
SnmpEngine, CommunityData, UdpTransportTarget,
|
|
|
|
|
|
ContextData, ObjectType, ObjectIdentity, getCmd
|
|
|
|
|
|
)
|
|
|
|
|
|
HAS_PYSNMP = True
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
HAS_PYSNMP = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# 1. GET: Lấy cấu hình giám sát của thiết bị
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# Luồng đi:
|
|
|
|
|
|
# GET /api/devices/{device_id}/monitor-config
|
|
|
|
|
|
# → Controller gọi hàm này
|
|
|
|
|
|
# → Kiểm tra device tồn tại (DeviceNotFoundException nếu không)
|
|
|
|
|
|
# → Kiểm tra config tồn tại (MonitorConfigNotFoundException nếu không)
|
|
|
|
|
|
# → Trả về dict cấu hình
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
def get_monitor_config_service(device_id):
|
|
|
|
|
|
# Bước 1: Kiểm tra thiết bị có tồn tại trong hệ thống không
|
|
|
|
|
|
device = find_device_by_id(device_id)
|
|
|
|
|
|
if not device:
|
|
|
|
|
|
raise DeviceNotFoundException(device_id)
|
|
|
|
|
|
|
|
|
|
|
|
# Bước 2: Lấy cấu hình giám sát từ DB
|
|
|
|
|
|
# (Bản ghi monitor_config được tạo tự động khi tạo device trong module device)
|
|
|
|
|
|
config = find_monitor_config_by_device_id(device_id)
|
|
|
|
|
|
if not config:
|
|
|
|
|
|
raise MonitorConfigNotFoundException(device_id)
|
|
|
|
|
|
|
|
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# 2. PUT: Cập nhật cấu hình giám sát
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# Luồng đi:
|
|
|
|
|
|
# PUT /api/devices/{device_id}/monitor-config
|
|
|
|
|
|
# → Controller validate body bằng UpdateMonitorConfigSchema
|
|
|
|
|
|
# → Service gọi hàm này
|
|
|
|
|
|
# → Kiểm tra device + config tồn tại
|
|
|
|
|
|
# → Cập nhật DB (Repository)
|
|
|
|
|
|
# → Gọi Scheduler reschedule_job để áp dụng tần suất mới NGAY LẬP TỨC
|
|
|
|
|
|
# → Trả về config đã cập nhật
|
|
|
|
|
|
#
|
|
|
|
|
|
# Ví dụ: Người dùng đổi ping_interval từ 60s → 30s
|
|
|
|
|
|
# → DB được cập nhật
|
|
|
|
|
|
# → Scheduler reschedule job: lần kiểm tra tiếp theo sẽ chạy sau 30s thay vì 60s
|
|
|
|
|
|
# → KHÔNG cần restart server
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
def update_monitor_config_service(device_id, data):
|
|
|
|
|
|
# Bước 1: Kiểm tra thiết bị tồn tại
|
|
|
|
|
|
device = find_device_by_id(device_id)
|
|
|
|
|
|
if not device:
|
|
|
|
|
|
raise DeviceNotFoundException(device_id)
|
|
|
|
|
|
|
|
|
|
|
|
# Bước 2: Kiểm tra config tồn tại
|
|
|
|
|
|
existing_config = find_monitor_config_by_device_id(device_id)
|
|
|
|
|
|
if not existing_config:
|
|
|
|
|
|
raise MonitorConfigNotFoundException(device_id)
|
|
|
|
|
|
|
|
|
|
|
|
# Bước 3: Cập nhật vào DB
|
|
|
|
|
|
updated_config = update_monitor_config_db(device_id, data)
|
|
|
|
|
|
|
|
|
|
|
|
# Bước 4: Thông báo cho Scheduler cập nhật lại job
|
|
|
|
|
|
# reschedule_device_monitoring_job sẽ đọc config mới từ DB
|
|
|
|
|
|
# và áp dụng interval mới cho job ping/snmp
|
|
|
|
|
|
reschedule_device_monitoring_job(device_id, updated_config)
|
|
|
|
|
|
|
|
|
|
|
|
return updated_config
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# 3. POST: Test kết nối (Ping hoặc SNMP)
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# Luồng đi:
|
|
|
|
|
|
# POST /api/devices/{device_id}/monitor-config/test
|
|
|
|
|
|
# → Controller validate body bằng TestConnectionSchema
|
|
|
|
|
|
# → Service gọi hàm này
|
|
|
|
|
|
# → Lấy IP thiết bị từ DB
|
|
|
|
|
|
# → Chạy Ping test (nếu test_ping=True)
|
|
|
|
|
|
# → Chạy SNMP test (nếu test_snmp=True)
|
|
|
|
|
|
# → Trả về kết quả chi tiết {ping_result, snmp_result}
|
|
|
|
|
|
#
|
|
|
|
|
|
# Cơ chế Fallback cho Ping:
|
|
|
|
|
|
# Ưu tiên 1: icmplib (thư viện Python, cần cài pip install icmplib)
|
|
|
|
|
|
# → async_ping() hoặc ping() với privileged=False
|
|
|
|
|
|
# Ưu tiên 2: Lệnh ping hệ điều hành (subprocess)
|
|
|
|
|
|
# → Chạy "ping -c 3 <ip>" (Linux/Mac) hoặc "ping -n 3 <ip>" (Windows)
|
|
|
|
|
|
# → Parse kết quả từ stdout
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
def test_connection_service(device_id, test_params):
|
|
|
|
|
|
# Bước 1: Kiểm tra thiết bị tồn tại và lấy IP
|
|
|
|
|
|
device = find_device_by_id(device_id)
|
|
|
|
|
|
if not device:
|
|
|
|
|
|
raise DeviceNotFoundException(device_id)
|
|
|
|
|
|
|
|
|
|
|
|
ip_address = device["ip_address"]
|
|
|
|
|
|
results = {}
|
|
|
|
|
|
|
|
|
|
|
|
# Bước 2: Test Ping nếu được yêu cầu
|
|
|
|
|
|
if test_params.get("test_ping", True):
|
|
|
|
|
|
ping_count = test_params.get("ping_count", 3)
|
|
|
|
|
|
ping_timeout = test_params.get("ping_timeout", 5)
|
|
|
|
|
|
results["ping_result"] = _run_ping_test(ip_address, ping_count, ping_timeout)
|
|
|
|
|
|
|
|
|
|
|
|
# Bước 3: Test SNMP nếu được yêu cầu
|
|
|
|
|
|
if test_params.get("test_snmp", False):
|
|
|
|
|
|
snmp_params = {
|
|
|
|
|
|
"community": test_params.get("snmp_community", "public"),
|
|
|
|
|
|
"version": test_params.get("snmp_version", "v2c"),
|
|
|
|
|
|
"port": test_params.get("snmp_port", 161),
|
|
|
|
|
|
"timeout": test_params.get("snmp_timeout", 5)
|
|
|
|
|
|
}
|
|
|
|
|
|
results["snmp_result"] = _run_snmp_test(ip_address, snmp_params)
|
|
|
|
|
|
|
|
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# PRIVATE: Hàm chạy Ping test
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# Cơ chế:
|
|
|
|
|
|
# 1. Thử dùng icmplib trước (nhanh, chính xác, có RTT)
|
|
|
|
|
|
# → privileged=False: không cần quyền root, dùng UDP probe
|
|
|
|
|
|
# 2. Nếu icmplib không có hoặc lỗi → Fallback sang subprocess
|
|
|
|
|
|
# → Chạy lệnh ping của hệ điều hành qua subprocess.run()
|
|
|
|
|
|
# → Parse returncode: 0 = thành công, != 0 = thất bại
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
def _run_ping_test(ip_address, count, timeout):
|
|
|
|
|
|
# ─── Ưu tiên 1: Dùng icmplib ─────────────────────
|
|
|
|
|
|
if HAS_ICMPLIB:
|
|
|
|
|
|
try:
|
|
|
|
|
|
# privileged=False: Không cần quyền root
|
|
|
|
|
|
# Trên macOS/Linux không root, icmplib sẽ dùng UDP probe
|
|
|
|
|
|
result = icmp_ping(
|
|
|
|
|
|
ip_address,
|
|
|
|
|
|
count=count,
|
|
|
|
|
|
timeout=timeout,
|
|
|
|
|
|
privileged=False
|
|
|
|
|
|
)
|
|
|
|
|
|
return {
|
|
|
|
|
|
"status": "up" if result.is_alive else "down",
|
|
|
|
|
|
"method": "icmplib",
|
|
|
|
|
|
"packets_sent": result.packets_sent,
|
|
|
|
|
|
"packets_received": result.packets_received,
|
|
|
|
|
|
"packet_loss": result.packet_loss, # 0.0 ~ 1.0
|
|
|
|
|
|
"avg_rtt_ms": round(result.avg_rtt, 2), # Thời gian phản hồi trung bình (ms)
|
|
|
|
|
|
"min_rtt_ms": round(result.min_rtt, 2),
|
|
|
|
|
|
"max_rtt_ms": round(result.max_rtt, 2)
|
|
|
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
# icmplib có nhưng gặp lỗi (ví dụ: permission denied)
|
|
|
|
|
|
# → Fallback sang subprocess
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# ─── Ưu tiên 2: Fallback sang lệnh ping hệ điều hành ───
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Xác định tham số theo hệ điều hành
|
|
|
|
|
|
# Windows dùng -n (number), Linux/macOS dùng -c (count)
|
|
|
|
|
|
param = "-n" if platform.system().lower() == "windows" else "-c"
|
|
|
|
|
|
timeout_param = "-w" if platform.system().lower() == "windows" else "-W"
|
|
|
|
|
|
|
|
|
|
|
|
cmd = ["ping", param, str(count), timeout_param, str(timeout), ip_address]
|
|
|
|
|
|
|
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
|
cmd,
|
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
|
|
timeout=timeout * count + 5 # Timeout tổng = timeout mỗi gói × số gói + buffer
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
is_alive = result.returncode == 0
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"status": "up" if is_alive else "down",
|
|
|
|
|
|
"method": "system_ping",
|
|
|
|
|
|
"detail": result.stdout.decode("utf-8", errors="replace")[:500]
|
|
|
|
|
|
# Giới hạn 500 ký tự đầu ra để tránh response quá lớn
|
|
|
|
|
|
}
|
|
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"status": "down",
|
|
|
|
|
|
"method": "system_ping",
|
|
|
|
|
|
"detail": f"Ping timeout after {timeout * count + 5} seconds"
|
|
|
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"status": "error",
|
|
|
|
|
|
"method": "system_ping",
|
|
|
|
|
|
"detail": str(e)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# PRIVATE: Hàm chạy SNMP test
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# Cơ chế:
|
|
|
|
|
|
# 1. Dùng pysnmp gửi SNMP GET request lấy sysDescr (OID: 1.3.6.1.2.1.1.1.0)
|
|
|
|
|
|
# → Đây là OID tiêu chuẩn mà MỌI thiết bị SNMP đều hỗ trợ
|
|
|
|
|
|
# → Nếu lấy được giá trị → thiết bị Up + hỗ trợ SNMP
|
|
|
|
|
|
# 2. Nếu pysnmp không có → trả về lỗi yêu cầu cài đặt
|
|
|
|
|
|
#
|
|
|
|
|
|
# Tham số SNMP:
|
|
|
|
|
|
# - community: chuỗi xác thực (mặc định "public")
|
|
|
|
|
|
# - version: phiên bản SNMP (v1, v2c, v3)
|
|
|
|
|
|
# - port: cổng SNMP trên thiết bị (mặc định 161)
|
|
|
|
|
|
# - timeout: thời gian chờ phản hồi (giây)
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
def _run_snmp_test(ip_address, snmp_params):
|
|
|
|
|
|
if not HAS_PYSNMP:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"status": "error",
|
|
|
|
|
|
"method": "snmp",
|
|
|
|
|
|
"detail": "pysnmp library is not installed. Run: pip install pysnmp"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
community = snmp_params.get("community", "public")
|
|
|
|
|
|
port = snmp_params.get("port", 161)
|
|
|
|
|
|
timeout_val = snmp_params.get("timeout", 5)
|
|
|
|
|
|
|
|
|
|
|
|
# Xác định mpModel (SNMP version) cho pysnmp
|
|
|
|
|
|
# v1 → mpModel=0, v2c → mpModel=1
|
|
|
|
|
|
version = snmp_params.get("version", "v2c")
|
|
|
|
|
|
mp_model = 0 if version == "v1" else 1
|
|
|
|
|
|
|
|
|
|
|
|
# Gửi SNMP GET request để lấy sysDescr
|
|
|
|
|
|
# OID: 1.3.6.1.2.1.1.1.0 = iso.org.dod.internet.mgmt.mib-2.system.sysDescr.0
|
|
|
|
|
|
# Đây là mô tả hệ thống — mọi thiết bị SNMP đều phải trả lời OID này
|
|
|
|
|
|
error_indication, error_status, error_index, var_binds = next(
|
|
|
|
|
|
getCmd(
|
|
|
|
|
|
SnmpEngine(),
|
|
|
|
|
|
CommunityData(community, mpModel=mp_model),
|
|
|
|
|
|
UdpTransportTarget((ip_address, port), timeout=timeout_val, retries=1),
|
|
|
|
|
|
ContextData(),
|
|
|
|
|
|
ObjectType(ObjectIdentity("1.3.6.1.2.1.1.1.0")) # sysDescr
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Phân tích kết quả
|
|
|
|
|
|
if error_indication:
|
|
|
|
|
|
# Lỗi transport: timeout, không kết nối được, v.v.
|
|
|
|
|
|
return {
|
|
|
|
|
|
"status": "down",
|
|
|
|
|
|
"method": "snmp",
|
|
|
|
|
|
"detail": str(error_indication)
|
|
|
|
|
|
}
|
|
|
|
|
|
elif error_status:
|
|
|
|
|
|
# Lỗi SNMP protocol: OID không tồn tại, quyền truy cập bị từ chối, v.v.
|
|
|
|
|
|
return {
|
|
|
|
|
|
"status": "down",
|
|
|
|
|
|
"method": "snmp",
|
|
|
|
|
|
"detail": f"SNMP error: {error_status.prettyPrint()} at {error_index}"
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Thành công! Thiết bị phản hồi SNMP
|
|
|
|
|
|
snmp_data = {}
|
|
|
|
|
|
for var_bind in var_binds:
|
|
|
|
|
|
oid = str(var_bind[0])
|
|
|
|
|
|
value = str(var_bind[1])
|
|
|
|
|
|
snmp_data[oid] = value
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"status": "up",
|
|
|
|
|
|
"method": "snmp",
|
|
|
|
|
|
"snmp_data": snmp_data,
|
|
|
|
|
|
"detail": f"SNMP {version} response received successfully"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"status": "error",
|
|
|
|
|
|
"method": "snmp",
|
|
|
|
|
|
"detail": f"SNMP test failed: {str(e)}"
|
|
|
|
|
|
}
|