feat: 平台预设字典+凭证加密+分库备份+Agent采集+移动端优先前端重做
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
"""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()]
|
||||
@@ -0,0 +1,47 @@
|
||||
"""VPS Agent 主入口:采集本机信息并上报到中心服务器
|
||||
|
||||
环境变量(建议写入 /etc/vps-agent.env):
|
||||
VPS_MANAGER_URL 中心地址,如 http://100.89.0.11:8000
|
||||
VPS_ASSET_ID 本机对应的资产 ID
|
||||
VPS_AGENT_KEY Agent Key(中心配置了 AGENT_KEY 时必填)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import httpx
|
||||
|
||||
from collector import collect_metrics, collect_security, collect_server_info
|
||||
|
||||
|
||||
def main() -> int:
|
||||
url = os.environ.get("VPS_MANAGER_URL", "").rstrip("/")
|
||||
asset_id = os.environ.get("VPS_ASSET_ID", "")
|
||||
key = os.environ.get("VPS_AGENT_KEY", "")
|
||||
|
||||
if not url or not asset_id:
|
||||
print("错误:需设置 VPS_MANAGER_URL 和 VPS_ASSET_ID")
|
||||
return 1
|
||||
|
||||
payload = {
|
||||
"asset_id": int(asset_id),
|
||||
"metrics": collect_metrics(),
|
||||
"server_info": collect_server_info(),
|
||||
"security": collect_security(),
|
||||
}
|
||||
headers = {"X-Agent-Key": key} if key else {}
|
||||
|
||||
try:
|
||||
resp = httpx.post(
|
||||
f"{url}/api/agent/report", json=payload, headers=headers, timeout=30
|
||||
)
|
||||
print(f"上报完成 HTTP {resp.status_code}: {resp.text}")
|
||||
resp.raise_for_status()
|
||||
return 0
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f"上报失败: {e}")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,10 @@
|
||||
# VPS Agent 环境变量示例(复制为 /etc/vps-agent.env)
|
||||
|
||||
# 中心服务器地址(Tailscale 内网)
|
||||
VPS_MANAGER_URL=http://100.89.0.11:8000
|
||||
|
||||
# 本机在资产管理系统中对应的资产 ID
|
||||
VPS_ASSET_ID=1
|
||||
|
||||
# Agent Key(中心 .env 配置了 AGENT_KEY 时必填,需一致)
|
||||
VPS_AGENT_KEY=
|
||||
@@ -0,0 +1,13 @@
|
||||
# VPS Agent 服务(oneshot,由 timer 触发)
|
||||
# 部署位置:/etc/systemd/system/vps-agent.service
|
||||
# 需先部署 collector.py + reporter.py 到 /opt/vps-agent/,并配置 /etc/vps-agent.env
|
||||
|
||||
[Unit]
|
||||
Description=VPS Manager Agent (collect & report)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
EnvironmentFile=-/etc/vps-agent.env
|
||||
ExecStart=/usr/bin/python3 /opt/vps-agent/reporter.py
|
||||
@@ -0,0 +1,13 @@
|
||||
# VPS Agent 定时器(每 5 分钟采集上报一次)
|
||||
# 部署位置:/etc/systemd/system/vps-agent.timer
|
||||
|
||||
[Unit]
|
||||
Description=Run VPS Manager Agent every 5 minutes
|
||||
|
||||
[Timer]
|
||||
OnBootSec=1min
|
||||
OnUnitActiveSec=5min
|
||||
Unit=vps-agent.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
Reference in New Issue
Block a user