机架管理前后端(rack/):FastAPI + SQLite + Vue3,经统一门户上线
- 后端 prrack:MQTT(phonerail-backend @ 127.0.0.1:1884)↔ 遥测入库 / 事件增量同步 / WebSocket 推送; 命令白名单 + 重传同 seq;审计记门户 X-Auth-User(只绑 127.0.0.1,main.py 拒绝非回环)。 - 自动恢复:只看电流(平线 / 过低,服务器够不到手机 adb),先长按电源再断电重启, 每小时限次后停手;默认逐台关闭。adb 探活接口 recovery.Prober 预留。 - 前端:总览 / 板子页(五路操作、电流电压曲线)/ 事件 / 操作记录 / 自动恢复;手机宽度可用。 - 部署 deploy/deploy.sh:服务器 Python 3.8 上先跑测试再替换;systemd phonerail-rack,每日备份。 - 门户应用 id 16、key rack(phonerail 已被需求文档站占用),授权 admin + staff。 测试 26 例(本机 3.14 / 服务器 3.8 均过)。线上板 b81f3fc9c8bc 在线,WS 经门户 101 + 1 条/s 遥测。 Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -0,0 +1,111 @@
|
||||
"""入口。
|
||||
|
||||
生产(服务器上,systemd 起):
|
||||
python -m prrack.main --db /root/phonerail-rack/data/rack.db \\
|
||||
--mqtt-env /root/phonerail/mqtt.env --static /root/phonerail-rack/frontend/dist
|
||||
本机开发(不连 broker,起 N 块模拟板子,每秒一条遥测):
|
||||
python -m prrack.main --sim 2 --db dev.db --static ../frontend/dist
|
||||
|
||||
⛔ 只许绑回环地址:api.py 按 L2 信任门户注入的 X-Auth-User,这个信任只在「外面够不着本进程」时成立。
|
||||
"""
|
||||
import argparse
|
||||
import asyncio
|
||||
import ipaddress
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import uvicorn
|
||||
|
||||
from .api import create_app
|
||||
from .bus import MemBroker, MemBus, PahoBus
|
||||
from .db import Db, now_ms
|
||||
from .hub import Hub
|
||||
from .recovery import RecoveryManager
|
||||
from .simboard import HUNG, SimBoard
|
||||
|
||||
log = logging.getLogger("prrack")
|
||||
|
||||
|
||||
def read_env(path: str) -> dict:
|
||||
out = {}
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
k, v = line.split("=", 1)
|
||||
out[k.strip()] = v.strip().strip('"')
|
||||
return out
|
||||
|
||||
|
||||
def build(args):
|
||||
db = Db(args.db)
|
||||
sims = []
|
||||
if args.sim:
|
||||
broker = MemBroker()
|
||||
bus = MemBus(broker)
|
||||
for i in range(args.sim):
|
||||
present = (True, True, True, False, True) if i == 0 else (True,) * 5
|
||||
sb = SimBoard(MemBus(broker), "5157ab%06x" % i, present=present, seed=i + 1)
|
||||
if i == 0:
|
||||
sb.set_phone(2, HUNG) # 留一台「卡死」的,给自动恢复看
|
||||
sims.append(sb)
|
||||
else:
|
||||
env = read_env(args.mqtt_env)
|
||||
bus = PahoBus(args.mqtt_host, args.mqtt_port, env.get("BACKEND_USER", "phonerail-backend"),
|
||||
env["BACKEND_PASS"], "phonerail-rack-%d" % os.getpid())
|
||||
hub = Hub(db, bus)
|
||||
rec = RecoveryManager(hub, now_ms)
|
||||
|
||||
async def up():
|
||||
await hub.start()
|
||||
for sb in sims:
|
||||
sb.start()
|
||||
if sims:
|
||||
async def ticker():
|
||||
while True:
|
||||
for sb in sims:
|
||||
sb.tick(now_ms())
|
||||
await asyncio.sleep(1)
|
||||
asyncio.ensure_future(ticker())
|
||||
|
||||
async def down():
|
||||
await hub.stop()
|
||||
db.close()
|
||||
|
||||
static = args.static if args.static and os.path.isdir(args.static) else None
|
||||
app = create_app(hub, rec, static, startup=[up], shutdown=[down])
|
||||
return app
|
||||
|
||||
|
||||
def ws_library_ok() -> bool:
|
||||
for mod in ("websockets", "wsproto"):
|
||||
try:
|
||||
__import__(mod)
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description="PhoneRail 机架管理后端")
|
||||
ap.add_argument("--db", default="rack.db")
|
||||
ap.add_argument("--static", default=None)
|
||||
ap.add_argument("--bind", default="127.0.0.1")
|
||||
ap.add_argument("--port", type=int, default=7764)
|
||||
ap.add_argument("--mqtt-host", default="127.0.0.1")
|
||||
ap.add_argument("--mqtt-port", type=int, default=1884)
|
||||
ap.add_argument("--mqtt-env", default="/root/phonerail/mqtt.env")
|
||||
ap.add_argument("--sim", type=int, default=0, help="不连 broker,起 N 块模拟板子")
|
||||
args = ap.parse_args(argv)
|
||||
if not ipaddress.ip_address(args.bind).is_loopback:
|
||||
sys.exit("拒绝绑 %s:只许回环地址(见文件头)" % args.bind)
|
||||
if not ws_library_ok():
|
||||
sys.exit("没装 websockets / wsproto:uvicorn 会把 /api/ws 当普通 GET,界面能开但永远不刷新")
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
||||
uvicorn.run(build(args), host=args.bind, port=args.port, log_level="info")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
在新工单中引用
屏蔽一个用户