From 594f02a49d799e17e3f6c4f9af54d724272e3975 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Thu, 27 Aug 2026 11:08:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20foms=5Fapp=20=E7=BA=AF=E4=BC=9E?= =?UTF-8?q?=EF=BC=88=E5=BA=94=E7=94=A8=E5=A3=B3=EF=BC=89=E2=80=94=20?= =?UTF-8?q?=E7=BB=84=E8=A3=85=E4=B8=8E=E5=90=AF=E5=8A=A8=EF=BC=8C=E6=97=A0?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 业务全部在独立模块仓库(foms / dbbackup / filesync / hostwatch), 本仓只做:入口组装、build、conf、start/stop、首页、RBAC 权限注册。 内容: · app/foms.py 入口,按序 load 全部模块 · app/global_func.py get_module_dbname(统一库 foms) · build.sh clone+安装全部模块、生成 DDL/CRUD、挂载 wwwroot · conf/config.json website.paths:foms 根路径 + 三模块前缀 · init/data.yaml 全部 codes(含拆分后新增的 codes) · scripts/load_path.py 重写为新 URL 布局 · wwwroot/index.ui 首页(指向各模块前缀路径) 从 foms 仓库迁出的应用层文件(app/build/conf/start/stop/index.ui/data.yaml/ init_rbac/load_path)在 foms 侧已删除。 --- .env.example | 8 + .gitignore | 19 +++ README.md | 51 ++++++ app/foms.py | 45 ++++++ app/global_func.py | 12 ++ build.sh | 62 ++++++++ conf/config.json | 46 ++++++ init/data.yaml | 360 +++++++++++++++++++++++++++++++++++++++++++ scripts/init_rbac.py | 155 +++++++++++++++++++ scripts/load_path.py | 93 +++++++++++ start.sh | 6 + stop.sh | 9 ++ wwwroot/index.ui | 96 ++++++++++++ 13 files changed, 962 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/foms.py create mode 100644 app/global_func.py create mode 100755 build.sh create mode 100644 conf/config.json create mode 100644 init/data.yaml create mode 100755 scripts/init_rbac.py create mode 100755 scripts/load_path.py create mode 100755 start.sh create mode 100755 stop.sh create mode 100644 wwwroot/index.ui diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c3df162 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# FOMS Environment Variables +export FOMS_PORT=9080 + +# Database +export MYSQL_ROOT_PASSWORD= + +# Agent Communication +export FOMS_AGENT_TOKEN= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f534480 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +build/ +dist/ +*.pid +logs/ +files/ +pkgs/ +bricks/ +py3/ +.env +# 模块 wwwroot 由 build 生成/软链,但伞仓自己的 index.ui 是手写静态页,须入库 +wwwroot/* +!wwwroot/index.ui +*.swp +*.swo +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..72b7ec1 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# foms_app — FOMS 纯伞(应用壳) + +本仓库是 FOMS 的**纯伞**:只做组装与启动,不含任何业务逻辑。 +业务全部在独立模块仓库,伞通过 `build.sh` 拉取、安装、软链、生成页面。 + +## 仓库职责边界 + +| 仓库 | 角色 | 内容 | +|---|---|---| +| **foms_app(本仓)** | 纯伞 | 入口 `app/foms.py`、`build.sh`、`conf/`、`start/stop.sh`、首页 `wwwroot/index.ui`、`init/data.yaml` | +| foms | 业务模块(核心) | 主备切换编排(六步状态机+防脑裂)、集群/节点/心跳/远程命令、Agent | +| dbbackup | 业务模块 | 数据库备份 + 日志同步(binlog 复制) | +| filesync | 业务模块 | 运行文件同步(rsync) | +| hostwatch | 业务模块 | 日志监控 + 日志分析 + 主机指标 | + +## 构建 + +```bash +./build.sh +``` + +build.sh 做的事(全部是组装,无业务): +1. 建 venv +2. clone + 安装基础包(apppublic/sqlor/ahserver/bricks-for-python/xls2ddl) +3. 构建 bricks 前端,软链到 ./bricks +4. clone + 安装鉴权模块(appbase/rbac) +5. clone + 安装**全部业务模块**(foms/dbbackup/filesync/hostwatch)+ 各生成 DDL +6. xls2ui 从各模块 `json/` 生成 CRUD 页面到各模块 `wwwroot/` +7. 把各模块 `wwwroot/` 软链到主 `wwwroot/<模块名>`(URL 前缀隔离:`/foms/`、`/dbbackup/`…) +8. 建运行时目录 + +## 运行 + +```bash +cp .env.example .env # 填 MYSQL_ROOT_PASSWORD / FOMS_AGENT_TOKEN / FOMS_PORT +./start.sh # 监听 FOMS_PORT(默认 9080) +./stop.sh +``` + +## 数据库 + +所有模块共享一个库 `foms`(`get_module_dbname()` 固定返回 'foms')。 +建库:依次执行各模块 `models/mysql.ddl.sql`(build 后生成)。 + +## 加载顺序(app/foms.py) + +``` +set_globalvariable → load_pybricks → load_appbase → load_rbac +→ load_foms(核心,注册切换编排器+健康判定器) +→ load_dbbackup → load_filesync → load_hostwatch +``` diff --git a/app/foms.py b/app/foms.py new file mode 100644 index 0000000..1045406 --- /dev/null +++ b/app/foms.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""FOMS_APP — 纯伞(应用壳)主入口。 + +本仓库只做组装与启动,业务逻辑全部在独立模块仓库: + · foms — 主备切换核心(集群/节点/切换编排/心跳/远程命令) + · dbbackup — 数据库备份 + 日志同步(binlog 复制) + · filesync — 运行文件同步(rsync) + · hostwatch — 日志监控 + 日志分析 + 主机指标 +""" +import os, sys + +# Add root so sibling modules are importable +root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, root_dir) + +from bricks_for_python.init import load_pybricks +from ahserver.webapp import webapp +from ahserver.serverenv import ServerEnv + +# Foundation modules +from rbac.init import load_rbac +from appbase.init import load_appbase + +# Business modules(核心 + 三个独立模块,均为独立仓库) +from foms.init import load_foms +from dbbackup.init import load_dbbackup +from filesync.init import load_filesync +from hostwatch.init import load_hostwatch + +from app.global_func import set_globalvariable + + +def init(): + set_globalvariable() + load_pybricks() + load_appbase() + load_rbac() + load_foms() # 核心:集群/节点/切换/心跳/远程命令 + load_dbbackup() # 数据库备份 + 日志同步(binlog 复制) + load_filesync() # 运行文件同步(rsync) + load_hostwatch() # 日志监控 + 日志分析 + 主机指标 + + +if __name__ == '__main__': + webapp(init) diff --git a/app/global_func.py b/app/global_func.py new file mode 100644 index 0000000..1e6c7d3 --- /dev/null +++ b/app/global_func.py @@ -0,0 +1,12 @@ +"""FOMS_APP global functions — injected into dspy context""" +from ahserver.serverenv import ServerEnv + + +def get_module_dbname(mname): + """All FOMS modules share one database.""" + return 'foms' + + +def set_globalvariable(): + env = ServerEnv() + env.get_module_dbname = get_module_dbname diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..99b87e5 --- /dev/null +++ b/build.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# foms_app 纯伞构建脚本 —— 只组装,无业务逻辑 +# 业务全部在独立模块仓库:foms / dbbackup / filesync / hostwatch +set -e +cdir=$(pwd) +echo "=== FOMS_APP Build(纯伞)===" + +# 1. Create venv +[ ! -d py3 ] && python3 -m venv py3 +source py3/bin/activate + +# 2. Install foundation packages +mkdir -p pkgs +for m in apppublic sqlor ahserver bricks-for-python xls2ddl; do + cd $cdir/pkgs + [ ! -d "$m" ] && git clone https://git.opencomputing.cn/yumoqing/$m + cd $m && $cdir/py3/bin/pip install -e . +done + +# 3. Build bricks frontend +cd $cdir/pkgs +[ ! -d "bricks" ] && git clone https://git.opencomputing.cn/yumoqing/bricks +cd bricks/bricks && ./build.sh +ln -sf $cdir/pkgs/bricks/dist $cdir/bricks + +# 4. Install auth modules +for m in appbase rbac; do + cd $cdir/pkgs + [ ! -d "$m" ] && git clone https://git.opencomputing.cn/yumoqing/$m + cd $m && $cdir/py3/bin/pip install -e . +done + +# 5. Install 全部业务模块(各自独立仓库,含核心 foms) +for m in foms dbbackup filesync hostwatch; do + cd $cdir/pkgs + [ ! -d "$m" ] && git clone https://git.opencomputing.cn/yumoqing/$m + cd $m && $cdir/py3/bin/pip install -e . + # 生成模块 DDL + if [ -d models ]; then + json2ddl mysql models/ > models/mysql.ddl.sql 2>/dev/null || echo " skip ddl($m)" + fi +done + +# 6. Generate CRUD UI(xls2ui 从各模块 json/ 生成到各模块 wwwroot/) +for m in foms dbbackup filesync hostwatch; do + md=$cdir/pkgs/$m + if [ -d "$md/json" ]; then + cd "$md/json" + for f in *.json; do + echo " xls2ui $m $f" + xls2ui -m ../models -o ../wwwroot "$m" "$f" 2>/dev/null || echo " skipped" + done + fi +done + +# 7. Create runtime dirs +# (模块 wwwroot 通过 conf/config.json 的 website.paths 直接挂载, +# foms 核心在根路径,三模块在 /dbbackup /filesync /hostwatch 前缀下) +cd $cdir +mkdir -p logs files + +echo "=== FOMS_APP Build Complete ===" diff --git a/conf/config.json b/conf/config.json new file mode 100644 index 0000000..b3c0f2e --- /dev/null +++ b/conf/config.json @@ -0,0 +1,46 @@ +{ + "password_key": "foms_secret_key_change_me", + "logger": { + "name": "foms", + "level": "INFO", + "file": "$[workdir]$/logs/foms.log" + }, + "filesroot": "$[workdir]$/files", + "databases": { + "foms": { + "driver": "aiomysql", + "kwargs": { + "host": "127.0.0.1", + "port": 3306, + "user": "root", + "password": "", + "db": "foms", + "charset": "utf8mb4", + "autocommit": true + } + } + }, + "website": { + "paths": [ + ["$[workdir]$/wwwroot", ""], + ["$[workdir]$/pkgs/foms/wwwroot", ""], + ["$[workdir]$/pkgs/dbbackup/wwwroot", "/dbbackup"], + ["$[workdir]$/pkgs/filesync/wwwroot", "/filesync"], + ["$[workdir]$/pkgs/hostwatch/wwwroot", "/hostwatch"], + ["$[workdir]$/bricks", "/bricks"] + ], + "port": 9080, + "indexes": ["index.ui"], + "session_max_time": 86400, + "session_issue_time": 3600, + "processors": [ + [".dspy", "dspy"], + [".ui", "bui"], + [".tmpl", "tmpl"], + [".js", "staticfile"], + [".css", "staticfile"], + [".png", "staticfile"], + [".svg", "staticfile"] + ] + } +} diff --git a/init/data.yaml b/init/data.yaml new file mode 100644 index 0000000..487d192 --- /dev/null +++ b/init/data.yaml @@ -0,0 +1,360 @@ +appcodes: +- id: cluster_status + name: 集群状态 + hierarchy_flg: 0 +- id: node_role + name: 节点角色 + hierarchy_flg: 0 +- id: node_status + name: 节点状态 + hierarchy_flg: 0 +- id: failover_mode + name: 切换触发模式 + hierarchy_flg: 0 +- id: cmd_status + name: 命令状态 + hierarchy_flg: 0 +- id: cmd_type + name: 命令类型 + hierarchy_flg: 0 +- id: step_status + name: 切换步骤状态 + hierarchy_flg: 0 +- id: sync_status + name: 同步状态 + hierarchy_flg: 0 +- id: sync_result + name: 同步结果 + hierarchy_flg: 0 +- id: backup_type + name: 备份类型 + hierarchy_flg: 0 +- id: backup_status + name: 备份记录状态 + hierarchy_flg: 0 +- id: match_type + name: 规则匹配类型 + hierarchy_flg: 0 +- id: severity + name: 告警级别 + hierarchy_flg: 0 +- id: event_status + name: 告警事件状态 + hierarchy_flg: 0 +- id: switchover_event_type + name: 切换事件类型 + hierarchy_flg: 0 +- id: switchover_trigger + name: 触发方式 + hierarchy_flg: 0 +- id: switchover_status + name: 切换执行状态 + hierarchy_flg: 0 +- id: org_type + name: 机构类型 + hierarchy_flg: 0 +- id: user_status + name: 用户状态 + hierarchy_flg: 0 +- id: log_level + name: 日志级别 + hierarchy_flg: 0 + +appcodes_kv: +- id: cluster_status_running + parentid: cluster_status + k: running + v: 运行中 +- id: cluster_status_stopped + parentid: cluster_status + k: stopped + v: 已停止 +- id: cluster_status_switching + parentid: cluster_status + k: switching + v: 切换中 +- id: cluster_status_error + parentid: cluster_status + k: error + v: 异常 + +- id: node_role_master + parentid: node_role + k: master + v: 主机 +- id: node_role_standby + parentid: node_role + k: standby + v: 备机 + +- id: node_status_online + parentid: node_status + k: online + v: 在线 +- id: node_status_offline + parentid: node_status + k: offline + v: 离线 +- id: node_status_error + parentid: node_status + k: error + v: 异常 + +- id: failover_mode_auto + parentid: failover_mode + k: auto + v: 全自动 +- id: failover_mode_semi + parentid: failover_mode + k: semi + v: 半自动(告警+人工确认) +- id: failover_mode_manual + parentid: failover_mode + k: manual + v: 纯手动 + +- id: cmd_status_pending + parentid: cmd_status + k: pending + v: 待执行 +- id: cmd_status_running + parentid: cmd_status + k: running + v: 执行中 +- id: cmd_status_success + parentid: cmd_status + k: success + v: 成功 +- id: cmd_status_failed + parentid: cmd_status + k: failed + v: 失败 + +- id: cmd_type_shell + parentid: cmd_type + k: shell + v: 普通命令 +- id: cmd_type_fence + parentid: cmd_type + k: fence + v: 隔离(停库+摘VIP) +- id: cmd_type_unfence + parentid: cmd_type + k: unfence + v: 恢复(起库+还VIP) +- id: cmd_type_promote + parentid: cmd_type + k: promote_master + v: 提升为主库 +- id: cmd_type_revert_promote + parentid: cmd_type + k: revert_promote + v: 降回从库 +- id: cmd_type_flip_sync + parentid: cmd_type + k: flip_sync + v: 反转同步方向 +- id: cmd_type_verify + parentid: cmd_type + k: verify + v: 切换后校验 + +- id: step_status_pending + parentid: step_status + k: pending + v: 待执行 +- id: step_status_running + parentid: step_status + k: running + v: 执行中 +- id: step_status_success + parentid: step_status + k: success + v: 成功 +- id: step_status_failed + parentid: step_status + k: failed + v: 失败 +- id: step_status_skipped + parentid: step_status + k: skipped + v: 跳过 + +- id: sync_status_not_configured + parentid: sync_status + k: not_configured + v: 未配置 +- id: sync_status_running + parentid: sync_status + k: running + v: 同步中 +- id: sync_status_error + parentid: sync_status + k: error + v: 异常 +- id: sync_status_failed + parentid: sync_status + k: failed + v: 失败 + +- id: sync_result_success + parentid: sync_result + k: success + v: 成功 +- id: sync_result_failed + parentid: sync_result + k: failed + v: 失败 +- id: sync_result_running + parentid: sync_result + k: running + v: 同步中 + +- id: backup_type_full + parentid: backup_type + k: full + v: 全量 +- id: backup_type_incremental + parentid: backup_type + k: incremental + v: 增量 + +- id: backup_status_running + parentid: backup_status + k: running + v: 执行中 +- id: backup_status_success + parentid: backup_status + k: success + v: 成功 +- id: backup_status_failed + parentid: backup_status + k: failed + v: 失败 +- id: backup_status_expired + parentid: backup_status + k: expired + v: 已过期 + +- id: match_type_keyword + parentid: match_type + k: keyword + v: 关键词 +- id: match_type_regex + parentid: match_type + k: regex + v: 正则 + +- id: severity_info + parentid: severity + k: info + v: 提示 +- id: severity_warning + parentid: severity + k: warning + v: 警告 +- id: severity_critical + parentid: severity + k: critical + v: 严重 + +- id: event_status_open + parentid: event_status + k: open + v: 未处理 +- id: event_status_closed + parentid: event_status + k: closed + v: 已关闭 + +- id: switchover_event_failover + parentid: switchover_event_type + k: failover + v: 故障切换 +- id: switchover_event_failback + parentid: switchover_event_type + k: failback + v: 故障切回 +- id: switchover_event_manual + parentid: switchover_event_type + k: manual + v: 手动操作 +- id: switchover_event_fault_detected + parentid: switchover_event_type + k: fault_detected + v: 故障检测 + +- id: switchover_trigger_auto + parentid: switchover_trigger + k: auto + v: 自动检测 +- id: switchover_trigger_auto_detect + parentid: switchover_trigger + k: auto_detect + v: 健康判定器检测 +- id: switchover_trigger_manual + parentid: switchover_trigger + k: manual + v: 手动触发 +- id: switchover_trigger_manual_confirm + parentid: switchover_trigger + k: manual_confirm + v: 人工确认 + +- id: switchover_status_pending + parentid: switchover_status + k: pending + v: 等待执行 +- id: switchover_status_detected + parentid: switchover_status + k: detected + v: 已检测到 +- id: switchover_status_in_progress + parentid: switchover_status + k: in_progress + v: 执行中 +- id: switchover_status_success + parentid: switchover_status + k: success + v: 成功 +- id: switchover_status_completed + parentid: switchover_status + k: completed + v: 已完成 +- id: switchover_status_failed + parentid: switchover_status + k: failed + v: 失败 +- id: switchover_status_rollback_failed + parentid: switchover_status + k: rollback_failed + v: 回滚失败(人工介入) + +- id: org_type_foms + parentid: org_type + k: foms_org + v: FOMS运维组织 +- id: user_status_active + parentid: user_status + k: 0 + v: 正常 +- id: user_status_disabled + parentid: user_status + k: 1 + v: 停用 +- id: log_level_info + parentid: log_level + k: INFO + v: 信息 +- id: log_level_warn + parentid: log_level + k: WARN + v: 警告 +- id: log_level_error + parentid: log_level + k: ERROR + v: 错误 +- id: log_level_crit + parentid: log_level + k: CRIT + v: 严重 diff --git a/scripts/init_rbac.py b/scripts/init_rbac.py new file mode 100755 index 0000000..d1bf9d7 --- /dev/null +++ b/scripts/init_rbac.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +""" +FOMS RBAC 初始化 — 创建机构类型、机构、角色、用户 + +用法: cd /d/ymq/repos/foms && source py3/bin/activate && python scripts/init_rbac.py +""" +import os, sys, asyncio + +# Add project root +root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, root) + +from appPublic.uniqueID import getID +from appPublic.timeUtils import curDateString +from sqlor.dbpools import DBPools +from ahserver.serverenv import ServerEnv + +DBNAME = 'foms' + +ORG_ID = 'foms_org_001' +ROLES = { + 'foms_superadmin': '超级管理员', + 'foms_operator': '运维操作员', + 'foms_viewer': '只读观察者', +} +USERS = { + 'admin': {'name': '系统管理员', 'password': 'Admin@123', 'role': 'foms_superadmin'}, + 'operator': {'name': '运维操作员', 'password': 'Ops@123456', 'role': 'foms_operator'}, +} + + +async def main(): + env = ServerEnv() + dbname = env.get_module_dbname('rbac') if hasattr(env, 'get_module_dbname') else DBNAME + db = DBPools() + + async with db.sqlorContext(dbname) as sor: + # 1. Create organization + existing = await sor.R('organization', {'id': ORG_ID}) + if existing: + print(f"[SKIP] organization {ORG_ID} already exists") + else: + await sor.C('organization', { + 'id': ORG_ID, + 'orgname': 'FOMS运维中心', + 'orgabbr': 'FOMS', + 'org_type': 'foms_org', + }) + print(f"[OK] organization created: FOMS运维中心") + + # 2. Create roles + for role_id, role_name in ROLES.items(): + existing = await sor.R('role', {'id': role_id}) + if existing: + print(f"[SKIP] role {role_id} exists") + continue + await sor.C('role', { + 'id': role_id, + 'orgtypeid': 'foms_org', + 'name': role_name, + }) + print(f"[OK] role created: {role_id} ({role_name})") + + # 3. Create users + for username, info in USERS.items(): + uid = f'user_{username}' + existing = await sor.R('users', {'username': username}) + if existing: + print(f"[SKIP] user {username} exists") + else: + encoded_pw = env.password_encode(info['password']) + await sor.C('users', { + 'id': uid, + 'username': username, + 'name': info['name'], + 'nick_name': info['name'], + 'password': encoded_pw, + 'orgid': ORG_ID, + 'user_status': '0', + 'created_at': curDateString(), + }) + print(f"[OK] user created: {username}") + + # 4. Assign role to user + ur_id = f'ur_{username}_{info["role"]}' + existing_ur = await sor.R('userrole', {'userid': uid, 'roleid': info['role']}) + if existing_ur: + print(f"[SKIP] userrole {username}→{info['role']} exists") + continue + await sor.C('userrole', { + 'id': ur_id, + 'userid': uid, + 'roleid': info['role'], + }) + print(f"[OK] userrole: {username} → {info['role']}") + + # 5. Assign permissions to roles + # Get all FOMS-specific permissions (paths not from rbac/appbase) + all_perms = await sor.sqlExe( + "SELECT id, path FROM permission WHERE path IN (" + "SELECT path FROM permission WHERE path LIKE '/api/%' OR path LIKE '/%_list%'" + ") ORDER BY path", {} + ) + # Most paths start with / for FOMS root-mapped module + foms_perms = [p for p in all_perms if hasattr(p, 'path')] + if not foms_perms: + print("[WARN] No FOMS permissions found — run scripts/load_path.py first") + else: + # superadmin: all FOMS paths + # operator: all paths (can trigger switchover) + # viewer: read-only (no create/update/delete) + for perm in foms_perms: + pid = getattr(perm, 'id', '') + path = getattr(perm, 'path', '') + is_write = any(x in path for x in ['create', 'update', 'delete', 'trigger']) + is_agent = 'agent_' in path + + # superadmin gets everything + await _add_roleperm(sor, 'foms_superadmin', pid, path) + + # operator gets everything except agent paths (internal) + if not is_agent: + await _add_roleperm(sor, 'foms_operator', pid, path) + + # viewer gets read-only + if not is_write and not is_agent: + await _add_roleperm(sor, 'foms_viewer', pid, path) + + print(f"\n=== FOMS RBAC 初始化完成 ===") + print(f" 机构: FOMS运维中心 ({ORG_ID})") + print(f" 角色: {', '.join(f'{k}({v})' for k, v in ROLES.items())}") + print(f" 用户:") + for u, i in USERS.items(): + print(f" {u} / {i['password']} (角色: {i['role']})") + print(f"\n 角色权限分配:") + print(f" superadmin → 全部路径") + print(f" operator → 全部路径 (不含agent内部端点)") + print(f" viewer → 只读路径 (查看dashboard/列表)") + + +async def _add_roleperm(sor, role_id, perm_id, path): + """Add rolepermission if not exists""" + rp_id = f'rp_{role_id}_{perm_id}'[:32] + existing = await sor.R('rolepermission', {'roleid': role_id, 'permid': perm_id}) + if existing: + return + await sor.C('rolepermission', { + 'id': rp_id, + 'roleid': role_id, + 'permid': perm_id, + }) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/scripts/load_path.py b/scripts/load_path.py new file mode 100755 index 0000000..c075f54 --- /dev/null +++ b/scripts/load_path.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""FOMS_APP RBAC permission registration(纯伞层)。 + +URL 布局(见 conf/config.json website.paths): + · foms 核心 → 根路径 ""(/api/...、/clusters_list、/nodes_list ...) + · dbbackup → /dbbackup/... + · filesync → /filesync/... + · hostwatch → /hostwatch/... +""" +import os, sys + +# Find Sage root for set_role_perm.py +sage_root = None +for c in [os.path.expanduser("~/repos/sage"), os.path.expanduser("~/test/sage")]: + if os.path.isdir(os.path.join(c, "py3", "bin")): + sage_root = c + break +if not sage_root: + print("ERROR: Sage root not found") + sys.exit(1) + +sys.path.insert(0, os.path.join(sage_root, "py3", "bin")) + +from set_role_perm import set_path_perm + +# Paths accessible without login +PATHS_ANY = [ + "/", + "/index.ui", + "/api/login.dspy", +] + +# Paths requiring authentication +PATHS_LOGINED = [ + # ── foms 核心(根路径)── + "/api/clusters_create.dspy", + "/api/clusters_update.dspy", + "/api/clusters_delete.dspy", + "/api/nodes_create.dspy", + "/api/nodes_update.dspy", + "/api/nodes_delete.dspy", + "/api/trigger_switchover.dspy", + "/api/trigger_switchback.dspy", + "/api/dashboard_stats.dspy", + "/api/exec_remote_command.dspy", + "/clusters_list", + "/clusters_list/index.ui", + "/nodes_list", + "/nodes_list/index.ui", + # ── dbbackup ── + "/dbbackup/dbbackup_replications_list", + "/dbbackup/dbbackup_replications_list/index.ui", + "/dbbackup/dbbackup_jobs_list", + "/dbbackup/dbbackup_jobs_list/index.ui", + "/dbbackup/dbbackup_records_list", + "/dbbackup/dbbackup_records_list/index.ui", + "/dbbackup/api/run_backup.dspy", + "/dbbackup/api/cleanup_expired_backups.dspy", + # ── filesync ── + "/filesync/filesync_directories_list", + "/filesync/filesync_directories_list/index.ui", + # ── hostwatch ── + "/hostwatch/hostwatch_logs_list", + "/hostwatch/hostwatch_logs_list/index.ui", + "/hostwatch/hostwatch_rules_list", + "/hostwatch/hostwatch_rules_list/index.ui", + "/hostwatch/hostwatch_events_list", + "/hostwatch/hostwatch_events_list/index.ui", + "/hostwatch/api/node_logs.dspy", + "/hostwatch/api/node_metrics_history.dspy", + "/hostwatch/api/analyze_logs.dspy", + "/hostwatch/api/acknowledge_event.dspy", +] + +# Agent 端点:无登录鉴权(Agent 用 token),any +PATHS_ANY += [ + "/api/agent_heartbeat.dspy", + "/api/agent_poll_commands.dspy", + "/api/agent_report_command_result.dspy", + "/dbbackup/api/agent_report_db_health.dspy", + "/filesync/api/agent_report_dir_health.dspy", + "/hostwatch/api/agent_report_metrics.dspy", + "/hostwatch/api/agent_push_logs.dspy", +] + +if __name__ == '__main__': + for p in PATHS_ANY: + set_path_perm("any", p) + print(f" any {p}") + for p in PATHS_LOGINED: + set_path_perm("logined", p) + print(f" logined {p}") + print("Done. Restart FOMS_APP to apply.") diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..56e1b5f --- /dev/null +++ b/start.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +cd "$(dirname "$0")" +[ -f .env ] && source .env +nohup $PWD/py3/bin/python $PWD/app/foms.py -p ${FOMS_PORT:-9080} -w $PWD $PWD/logs/foms.out 2>&1 & +echo $! > foms.pid +echo "FOMS_APP started (PID: $(cat foms.pid)) on port ${FOMS_PORT:-9080}" diff --git a/stop.sh b/stop.sh new file mode 100755 index 0000000..c9974c6 --- /dev/null +++ b/stop.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +cd "$(dirname "$0")" +if [ -f foms.pid ]; then + pid=$(cat foms.pid) + kill $pid 2>/dev/null && echo "FOMS_APP stopped (PID: $pid)" || echo "Not running" + rm -f foms.pid +else + echo "No PID file found" +fi diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..1575e91 --- /dev/null +++ b/wwwroot/index.ui @@ -0,0 +1,96 @@ +{ + "widgettype": "VBox", + "options": {"width": "100%", "height": "100%", "padding": "24px", "backgroundColor": "#F5F7FA"}, + "subwidgets": [ + { + "widgettype": "Text", + "options": {"text": "FOMS — 主备切换管理系统", "fontSize": "28px", "fontWeight": "bold", "marginBottom": "8px"} + }, + { + "widgettype": "Text", + "options": {"text": "Failover Management System · 实时监控 · 自动切换 · 数据同步", "fontSize": "14px", "color": "#666", "marginBottom": "24px"} + }, + { + "widgettype": "ResponsableBox", + "options": {"gap": "16px", "minWidth": "220px", "marginBottom": "24px"}, + "subwidgets": [ + { + "widgettype": "VBox", + "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "borderRadius": "8px", "cursor": "pointer"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.foms_content", "options": {"url": "{{entire_url('clusters_list')}}"}, "mode": "replace"}], + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "集群管理", "fontSize": "16px", "fontWeight": "bold", "marginBottom": "8px"}}, + {"widgettype": "RefreshWidget", "options": {"interval": 30000, "data_url": "{{entire_url('api/dashboard_stats.dspy')}}"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "{{data.total_clusters}} 个集群", "fontSize": "14px", "color": "#666"}} + ] + } + ] + }, + { + "widgettype": "VBox", + "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "borderRadius": "8px", "cursor": "pointer"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.foms_content", "options": {"url": "{{entire_url('nodes_list')}}"}, "mode": "replace"}], + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "节点管理", "fontSize": "16px", "fontWeight": "bold", "marginBottom": "8px"}}, + {"widgettype": "RefreshWidget", "options": {"interval": 30000, "data_url": "{{entire_url('api/dashboard_stats.dspy')}}"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "{{data.nodes_online}} 在线 / {{data.nodes_offline}} 离线", "fontSize": "14px", "color": "#666"}} + ] + } + ] + }, + { + "widgettype": "VBox", + "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "borderRadius": "8px", "cursor": "pointer"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.foms_content", "options": {"url": "{{entire_url('dbbackup/dbbackup_replications_list')}}"}, "mode": "replace"}], + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "数据库备份/同步", "fontSize": "16px", "fontWeight": "bold", "marginBottom": "8px"}}, + {"widgettype": "RefreshWidget", "options": {"interval": 30000, "data_url": "{{entire_url('api/dashboard_stats.dspy')}}"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "{{data.dbs_synced}} 正常 / {{data.dbs_error}} 异常", "fontSize": "14px", "color": "#666"}} + ] + } + ] + }, + { + "widgettype": "VBox", + "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "borderRadius": "8px", "cursor": "pointer"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.foms_content", "options": {"url": "{{entire_url('filesync/filesync_directories_list')}}"}, "mode": "replace"}], + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "文件同步", "fontSize": "16px", "fontWeight": "bold", "marginBottom": "8px"}}, + {"widgettype": "RefreshWidget", "options": {"interval": 30000, "data_url": "{{entire_url('api/dashboard_stats.dspy')}}"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "{{data.dirs_synced}} 同步正常", "fontSize": "14px", "color": "#666"}} + ] + } + ] + }, + { + "widgettype": "VBox", + "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "borderRadius": "8px", "cursor": "pointer"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.foms_content", "options": {"url": "{{entire_url('hostwatch/hostwatch_logs_list')}}"}, "mode": "replace"}], + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "日志监控/分析", "fontSize": "16px", "fontWeight": "bold", "marginBottom": "8px"}}, + {"widgettype": "Text", "options": {"text": "日志采集 · 规则告警 · 指标", "fontSize": "14px", "color": "#666"}} + ] + } + ] + }, + { + "widgettype": "VBox", + "id": "foms_content", + "options": {"width": "100%", "flex": "1", "backgroundColor": "#FFFFFF", "borderRadius": "8px", "padding": "20px", "minHeight": "400px"}, + "subwidgets": [ + { + "widgettype": "Text", + "options": {"text": "欢迎使用 FOMS 主备切换管理系统", "fontSize": "18px", "color": "#999", "textAlign": "center", "marginTop": "80px"} + }, + { + "widgettype": "Text", + "options": {"text": "点击上方卡片进入各功能模块", "fontSize": "14px", "color": "#BBB", "textAlign": "center"} + } + ] + } + ] +}