Files
vps-manager/agent/collector.py
T

145 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""VPS Agent 采集模块
采集本机资源、服务器信息与基础安全状态。
依赖 psutil + 少量系统命令(ufw/iptables/sshd_config)。
"""
import platform
import socket
import subprocess
import time
import psutil
def collect_metrics() -> dict:
"""资源使用率(CPU/内存/磁盘/网络/负载/运行时长)"""
cpu = psutil.cpu_percent(interval=1)
mem = psutil.virtual_memory()
disk = psutil.disk_usage("/")
net = psutil.net_io_counters()
try:
load_1m = psutil.getloadavg()[0]
except (AttributeError, OSError):
load_1m = None
return {
"cpu_pct": round(cpu, 1),
"mem_pct": round(mem.percent, 1),
"disk_pct": round(disk.percent, 1),
"net_in_mb": round(net.bytes_recv / 1024 / 1024, 2),
"net_out_mb": round(net.bytes_sent / 1024 / 1024, 2),
"load_1m": round(load_1m, 2) if load_1m is not None else None,
"uptime_sec": int(time.time() - psutil.boot_time()),
}
def _get_ip() -> str:
"""获取本机出口 IP(不实际发包)"""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except Exception:
return ""
def collect_server_info() -> dict:
"""服务器基础信息"""
os_name = platform.system()
try:
with open("/etc/os-release") as f:
for line in f:
if line.startswith("PRETTY_NAME="):
os_name = line.split("=", 1)[1].strip().strip('"')
break
except Exception:
pass
return {
"hostname": socket.gethostname(),
"os": os_name,
"kernel": platform.release(),
"cpu_cores": psutil.cpu_count(),
"mem_total_gb": round(psutil.virtual_memory().total / 1024**3, 1),
"disk_total_gb": round(psutil.disk_usage("/").total / 1024**3, 1),
"public_ip": _get_ip(),
"status": "online",
}
def _check_ssh_config() -> dict:
"""检查 SSH 配置(root 登录 / 密码登录)"""
path = "/etc/ssh/sshd_config"
permit_root = "unknown"
password_auth = "unknown"
try:
with open(path) as f:
for line in f:
low = line.strip().lower()
if low.startswith("permitrootlogin"):
parts = low.split()
permit_root = parts[1] if len(parts) > 1 else "unknown"
elif low.startswith("passwordauthentication"):
parts = low.split()
password_auth = parts[1] if len(parts) > 1 else "unknown"
except Exception:
return {"check_item": "ssh_config", "status": "unknown",
"detail": "无法读取 sshd_config", "suggestion": None}
status = "pass"
suggestions = []
if permit_root == "yes":
status = "warn"
suggestions.append("设置 PermitRootLogin no 或 prohibit-password")
if password_auth == "yes":
status = "warn"
suggestions.append("关闭密码登录 PasswordAuthentication no,改用密钥")
return {
"check_item": "ssh_config",
"status": status,
"detail": f"PermitRootLogin={permit_root}, PasswordAuthentication={password_auth}",
"suggestion": "".join(suggestions) if suggestions else None,
}
def _check_firewall() -> dict:
"""检查防火墙状态"""
try:
out = subprocess.run(["ufw", "status"], capture_output=True, text=True, timeout=5)
if "Status: active" in out.stdout:
return {"check_item": "firewall", "status": "pass",
"detail": "ufw 已启用", "suggestion": None}
return {"check_item": "firewall", "status": "warn",
"detail": "ufw 未启用", "suggestion": "启用防火墙 ufw enable"}
except Exception:
try:
out = subprocess.run(["iptables", "-L", "-n"], capture_output=True, text=True, timeout=5)
if out.stdout.strip():
return {"check_item": "firewall", "status": "pass",
"detail": "iptables 有规则", "suggestion": None}
except Exception:
pass
return {"check_item": "firewall", "status": "unknown",
"detail": "未检测到 ufw/iptables", "suggestion": None}
def _check_listening_ports() -> dict:
"""统计监听端口"""
try:
conns = psutil.net_connections(kind="inet")
listening = sorted({c.laddr.port for c in conns if c.status == "LISTEN"})
detail = f"开放端口 {len(listening)} 个: {','.join(map(str, listening[:15]))}"
status = "pass" if len(listening) < 10 else "warn"
suggestion = "端口偏多,建议关闭非必要端口" if status == "warn" else None
return {"check_item": "listening_ports", "status": status,
"detail": detail, "suggestion": suggestion}
except Exception as e: # noqa: BLE001
return {"check_item": "listening_ports", "status": "unknown",
"detail": str(e), "suggestion": None}
def collect_security() -> list:
"""基础安全检查项"""
return [_check_ssh_config(), _check_firewall(), _check_listening_ports()]