当前位置: 首页 > news >正文

Rocky9的PXE服务器上安装一个web服务器

基于 Flask 的 PXE 测试机管理平台设计与实现

在大型测试环境下,我们经常需要快速管理多台测试机,获取状态信息、日志文件,甚至远程 SSH 登录。本文介绍如何在Rocky Linux 9PXE 服务器上搭建一个轻量级、Web 界面化的测试机管理平台。

该平台特点:

  • 自动记录所有连接到 PXE 服务器的测试机信息(IP / MAC / SN / 在线状态)

  • 能通过 Web 界面实时查看设备状态

  • 支持一键获取测试机日志

  • 支持通过 Web SSH 直接远程访问测试机(适合临时测试)

  • 针对测试环境优化,50 台内设备性能流畅

  • 无需长期运行,仅作为临时测试使用

系统架构设计

+----------------+ HTTP +-----------------+ | | <------------> | | | Web 浏览器 | | PXE Server | | (管理界面) | | Flask + WebSSH | | | | | +----------------+ +-----------------+ | | SSH / 日志访问 v +-----------------+ | 测试机群 | | (50 台内) | +-----------------+

功能模块

  1. 设备上报模块

    • 测试机通过curlPOST请求上报自己的 MAC 和 SN 到 PXE 服务器

    • Flask 保存最新状态,并带时间戳判断在线状态

  2. 设备状态展示模块

    • Web 前端显示总设备数、在线/离线数量

    • 支持按 IP/SN 排序

  3. 日志拉取模块

    • 支持点击按钮获取指定 IP 测试机日志

    • 使用 SSH 远程读取/var/log/test.log

  4. Web SSH 模块

    • 支持通过浏览器访问测试机终端(可使用 xterm.js 或简单 iframe)

    • 临时测试环境可直接在 iframe 内打开

  5. 后台清理模块

    • 自动删除长时间未上报的设备(默认 10 分钟)

目录结构如下

[root@192 pxe-dashboard]# tree . ├── server.py └── templates └── index.html

Python / Flask 代码

server.py

# /opt/pxe-dashboard/server.py from flask import Flask, request, jsonify, render_template import subprocess, threading, time from datetime import datetime app = Flask(__name__) devices = {} # key: MAC, value: dict lock = threading.Lock() OFFLINE_SECONDS = 30 # 判断离线时间 CLEANUP_SECONDS = 600 # 自动清理时间 # 首页 @app.route("/") def index(): return render_template("index.html") # 设备上报接口 @app.route("/api/report", methods=["POST"]) def report(): data = request.json if not data or "mac" not in data: return jsonify({"error": "invalid data"}), 400 now = datetime.now() with lock: devices[data["mac"]] = { "ip": request.remote_addr, "mac": data["mac"], "sn": data.get("sn", "Unknown"), "last_seen": now.strftime("%H:%M:%S"), "last_seen_ts": now.timestamp() } return jsonify({"status": "ok"}) # 获取设备列表 @app.route("/api/devices") def get_devices(): now_ts = time.time() result = [] with lock: for d in devices.values(): d_copy = d.copy() d_copy["status"] = "Online" if now_ts - d["last_seen_ts"] < OFFLINE_SECONDS else "Offline" result.append(d_copy) result.sort(key=lambda x: x["last_seen_ts"], reverse=True) return jsonify(result) # 拉取日志 @app.route("/get_log/<ip>") def get_log(ip): try: output = subprocess.check_output( ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=3", f"root@{ip}", "cat /var/log/test.log"], timeout=5 ) return "<pre>" + output.decode() + "</pre>" except Exception as e: return f"<pre>获取日志失败:\n{str(e)}</pre>" # WebSSH 访问 @app.route("/webssh/<ip>") def webssh(ip): # 这里假设本地部署了 webssh 服务器监听 7681 return f""" <iframe src="http://{request.host.split(':')[0]}:7681/?arg=ssh root@{ip}" width="100%" height="700"></iframe> """ # 后台清理线程 def cleanup_worker(): while True: time.sleep(60) now_ts = time.time() with lock: remove = [mac for mac, d in devices.items() if now_ts - d["last_seen_ts"] > CLEANUP_SECONDS] for mac in remove: del devices[mac] if __name__ == "__main__": threading.Thread(target=cleanup_worker, daemon=True).start() app.run(host="0.0.0.0", port=5000)

前端 HTML / JS

  • 支持 IP 和 SN 排序

  • 自动刷新列表(3 秒一次)

  • 点击按钮可获取日志或 WebSSH

<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>PXE Test Control Panel</title> <style> body { margin:0; font-family:"Segoe UI", Arial; background:#f3f4f6; } header { background:#111827; color:white; padding:18px; text-align:center; font-size:22px; } .stats { display:flex; gap:20px; padding:20px; } .card { flex:1; background:white; padding:20px; border-radius:10px; text-align:center; box-shadow:0 2px 6px rgba(0,0,0,0.1); } .table-container { padding:0 20px 40px 20px; } table { width:100%; border-collapse:collapse; background:white; border-radius:10px; overflow:hidden; box-shadow:0 2px 6px rgba(0,0,0,0.1); } th { background:#1f2937; color:white; padding:12px; cursor:pointer; } td { padding:10px; border-bottom:1px solid #eee; } tr:hover { background:#f9fafb; } .badge { padding:4px 10px; border-radius:20px; font-size:12px; font-weight:bold; } .online { background:#d1fae5; color:#065f46; } .offline { background:#fee2e2; color:#991b1b; } button { border:none; padding:6px 12px; border-radius:6px; cursor:pointer; font-size:13px; } .btn-log { background:#2563eb; color:white; } .btn-ssh { background:#10b981; color:white; } .sort-arrow { font-size:12px; margin-left:5px; } </style> <script> let currentSortField = "ip"; let sortAsc = true; function compareIP(a, b) { let pa = a.split('.').map(Number); let pb = b.split('.').map(Number); for(let i=0;i<4;i++){ if(pa[i] !== pb[i]) return pa[i] - pb[i]; } return 0; } function sortData(data){ data.sort((a,b)=>{ let result = 0; if(currentSortField === "ip"){ result = compareIP(a.ip, b.ip); } else if(currentSortField === "sn"){ result = a.sn.localeCompare(b.sn); } return sortAsc ? result : -result; }); } function setSort(field){ if(currentSortField === field){ sortAsc = !sortAsc; } else { currentSortField = field; sortAsc = true; } loadDevices(); } function loadDevices(){ fetch("/api/devices") .then(r=>r.json()) .then(data=>{ sortData(data); let tbody = document.getElementById("tbody"); tbody.innerHTML = ""; let online=0, offline=0; data.forEach(d=>{ if(d.status=="Online") online++; else offline++; let badgeClass = d.status=="Online" ? "online":"offline"; let row = ` <tr> <td>${d.ip}</td> <td>${d.mac}</td> <td>${d.sn}</td> <td><span class="badge ${badgeClass}">${d.status}</span></td> <td>${d.last_seen}</td> <td> <button class="btn-log" onclick="getLog('${d.ip}')">日志</button> <button class="btn-ssh" onclick="openSSH('${d.ip}')">SSH</button> </td> </tr>`; tbody.innerHTML += row; }); document.getElementById("total").innerText = data.length; document.getElementById("online").innerText = online; document.getElementById("offline").innerText = offline; updateArrows(); }); } function updateArrows(){ document.getElementById("ip_arrow").innerText = currentSortField=="ip" ? (sortAsc?"▲":"▼") : ""; document.getElementById("sn_arrow").innerText = currentSortField=="sn" ? (sortAsc?"▲":"▼") : ""; } function getLog(ip){ window.open("/get_log/"+ip,"_blank"); } function openSSH(ip){ window.open("/webssh/"+ip,"_blank"); } setInterval(loadDevices,3000); window.onload = loadDevices; </script> </head> <body> <header>PXE 测试控制平台</header> <div class="stats"> <div class="card"> <h2 id="total">0</h2> <p>总设备</p> </div> <div class="card"> <h2 id="online" style="color:#059669;">0</h2> <p>在线</p> </div> <div class="card"> <h2 id="offline" style="color:#dc2626;">0</h2> <p>离线</p> </div> </div> <div class="table-container"> <table> <thead> <tr> <th onclick="setSort('ip')"> IP <span id="ip_arrow" class="sort-arrow">▲</span> </th> <th>MAC</th> <th onclick="setSort('sn')"> SN <span id="sn_arrow" class="sort-arrow"></span> </th> <th>Status</th> <th>Last Seen</th> <th>操作</th> </tr> </thead> <tbody id="tbody"></tbody> </table> </div> </body> </html>

部署步骤

  1. 安装依赖:

dnf install -y python3-pip openssh-clients
pip3 install flask

安装wetty

dnf module list nodejs dnf module enable nodejs:18 -y dnf install nodejs -y node -v # 确认 >= 18 npm -v npm install -g wetty

关闭防火墙

# 停止 firewalld systemctl stop firewalld # 禁止开机自启 systemctl disable firewalld # 查看状态,确认已经关闭 systemctl status firewalld

启动wetty服务

打开/etc/systemd/system/wetty.service

[Unit] Description=WeTTY Web SSH After=network.target [Service] Type=simple ExecStart=/usr/local/bin/wetty --port 7681 --host 0.0.0.0 Restart=always User=root [Install] WantedBy=multi-user.target

保存后重载 systemd 并启动:

systemctl daemon-reload systemctl enable wetty systemctl start wetty systemctl status wetty
  1. 运行 Flask 服务:

cd /opt/pxe-dashboard
python3 server.py

  1. 测试机上报数据:

curl -X POST -H "Content-Type: application/json" \
-d '{"mac":"AA:BB:CC:DD:EE:FF","sn":"TEST001"}' \
http://PXE_SERVER_IP:5000/api/report

  1. 访问 Web 页面:

http://PXE_SERVER_IP:5000

  1. Web SSH:

  • 需要在 PXE 服务器本地或其他端口运行webssh服务(xterm.js 或 GateOne 可行)

  • 点击 SSH 按钮即可直接访问测试机

http://www.jsqmd.com/news/435461/

相关文章:

  • 2026长期染发,不伤身体的染发膏推荐 - 品牌排行榜
  • 2026网络安全应届生、春招面试必看教程!分享最近一次渗透测试岗位面试经验
  • 研究生毕业论文降AI率攻略:省时省钱的实用方案
  • 探寻2026年靠谱的劳动纠纷律师,北京市中闻律师事务所实力不容小觑 - 工业推荐榜
  • 2026年上海家电维修公司综合实力TOP5深度解析 - 2026年企业推荐榜
  • 婶可忍叔不可忍的AutoMapper,你还用吗?
  • 教育行业站群如何用Java定制文件上传下载的权限控制与日志审计?
  • 知网vs维普:毕业论文AIGC检测结果为什么差这么多?
  • 介绍了 Senparc.AI 所支撑的底层能力(持续更新中),从这个系列开始,我们将逐步探索其强大的扩展和落地能力。 本文介绍的案例 ...
  • 探讨2026年气泡纸压花机品牌选购,购买后交货时间要了解 - 工业品网
  • 2026喷雾干燥机采购白皮书:厂家排名、品牌梯队与实力供应商深度解析 - 品牌推荐大师1
  • AtCoder Weekday Contest 0010 Beta题解(AWC 0010 Beta A-E)
  • 2026年口碑好的全自动缓冲纸袋机制造厂推荐 售后有保障 - myqiye
  • 改变文档修改时间的 6 种实用技巧,无需额外工具
  • 文档的修改日期能改吗?快速改文档修改日期技巧
  • 聊聊板材存放架靠谱厂家,哪家性价比高且口碑好 - 工业设备
  • 振动流化床干燥机定制化解决方案:国内三家技术型厂家的核心竞争力分析 - 品牌推荐大师
  • SageMaker算法如何推动机器学习民主化
  • H2O-3反序列化漏洞分析(CVE-2025-6507CVE-2025-6544)
  • 受认可的美国海外仓靠谱不,收费标准是多少钱? - mypinpai
  • 真心不骗你! 自考必备的降AI率软件 千笔·专业降AIGC智能体 VS 学术猹
  • 日通机械基本信息有哪些,好用吗,在江苏地区口碑怎样? - 工业推荐榜
  • 2026中国的染发膏有比外国好的品牌吗?国货创新实力解析 - 品牌排行榜
  • ERROR 1820 (HY000): You must reset your password using ALTER USER statement‌
  • 聊聊2026年好用的劳动纠纷律师,证据精通文书起草维权有保障 - 工业品网
  • git批量删除本地多余分支
  • 深圳选到俄罗斯物流方案,易斯拉国际物流靠谱不? - 工业品牌热点
  • 如何查看文档真实修改时间?绕过表面显示的方法
  • 性价比高的劳动纠纷律师推荐,能处理索赔案件的有吗? - myqiye
  • 京东e卡回收热门平台流程复盘 - 京回收小程序