refactor: foms 改为纯业务模块,应用层迁至纯伞仓 foms_app
foms 只保留业务逻辑: · foms/ 包(init.py + failover.py 切换编排器) · models/(5 张表)、json/(CRUD 定义)、wwwroot/api/(端点) · scripts/(foms-agent 及部署脚本) · docs/deploy-operations.md 应用层迁出至 foms_app 纯伞仓: · app/(入口)、build.sh、start/stop.sh、conf/、init/data.yaml · scripts/init_rbac.py / load_path.py(重写为新 URL 布局) · wwwroot/index.ui(重写,指向各模块前缀路径) 同时修复 foms/__init__.py:移除对拆分时已删除函数的导出。
This commit is contained in:
parent
fa257f36b1
commit
dedb7bfc57
12
.gitignore
vendored
12
.gitignore
vendored
@ -7,14 +7,6 @@ dist/
|
||||
*.pid
|
||||
logs/
|
||||
files/
|
||||
pkgs/
|
||||
bricks/
|
||||
py3/
|
||||
models/mysql.ddl.sql
|
||||
.DS_Store
|
||||
.env
|
||||
*.swp
|
||||
*.swo
|
||||
# CRUD-generated directories (xls2ui)
|
||||
wwwroot/clusters_list/
|
||||
wwwroot/nodes_list/
|
||||
wwwroot/sync_databases_list/
|
||||
wwwroot/sync_directories_list/
|
||||
|
||||
76
README.md
76
README.md
@ -1,61 +1,41 @@
|
||||
# FOMS — Failover Management System
|
||||
# foms — 主备切换核心模块
|
||||
|
||||
主备机切换管理系统。运行在管理中心 C 上,管理主机 A 和备机 B 之间的:
|
||||
FOMS 的**领域核心**(业务逻辑层),独立可安装模块。
|
||||
应用壳(构建/配置/启动/首页)在纯伞仓 **foms_app**。
|
||||
|
||||
- **MySQL 主从复制** — 实时数据同步
|
||||
- **文件目录同步** — rsync 定期同步
|
||||
- **健康监控** — 节点心跳 + 复制状态
|
||||
- **故障切换** — 自动检测 A 故障 → 切换到 B
|
||||
- **故障切回** — A 修复后 → 切回 A
|
||||
## 内容
|
||||
|
||||
## 架构
|
||||
| 部分 | 文件 | 说明 |
|
||||
|---|---|---|
|
||||
| 包入口 | `foms/init.py` | 集群/节点 CRUD、心跳、仪表盘、远程命令队列、`load_foms()` |
|
||||
| 切换编排器 | `foms/failover.py` | 六步状态机 + 逆向回滚 + 防脑裂 + 健康判定器 |
|
||||
| 表定义 | `models/*.json` | clusters、nodes、switchover_logs、switchover_steps、node_commands |
|
||||
| CRUD 定义 | `json/*.json` | clusters_list、nodes_list |
|
||||
| 端点 | `wwwroot/api/*.dspy` | 心跳、命令、切换、仪表盘等 |
|
||||
| Agent | `scripts/foms-agent.py` | 被管主机守护进程(独立部署,非本包代码) |
|
||||
| 运维文档 | `docs/deploy-operations.md` | 部署 + 运维手册 |
|
||||
|
||||
## 切换闭环(2026-08-26 补全)
|
||||
|
||||
健康判定器(防抖告警,半自动)→ 人工确认 → 六步状态机:
|
||||
```
|
||||
管理中心 C (FOMS Web App)
|
||||
│
|
||||
├── 主机 A ←──MySQL复制/rsync──→ 备机 B
|
||||
│
|
||||
└── Agent 守护进程 (部署在 A & B)
|
||||
precheck → fence → promote → flip_sync → verify → converge
|
||||
```
|
||||
失败逆向回滚;防脑裂不变量:fence 先于 promote,fence 失败硬中止。
|
||||
详见 `docs/deploy-operations.md`。
|
||||
|
||||
## 快速开始
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# 1. 构建
|
||||
./build.sh
|
||||
|
||||
# 2. 配置 .env
|
||||
vim .env
|
||||
|
||||
# 3. 初始化数据库 (手动执行 models/mysql.ddl.sql)
|
||||
mysql -u root < models/mysql.ddl.sql
|
||||
|
||||
# 4. 注册RBAC权限
|
||||
cd py3/bin && python ../../scripts/load_path.py
|
||||
|
||||
# 5. 启动
|
||||
./start.sh
|
||||
pip install -e .
|
||||
```
|
||||
由 foms_app 的 build.sh 统一组装。`load_foms()` 时自动注册切换编排器
|
||||
并拉起健康判定器后台任务(幂等)。
|
||||
|
||||
## 模块
|
||||
## 邻接模块(独立仓库)
|
||||
|
||||
| 模块 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| 集群管理 | /clusters_list | 配置主备集群 |
|
||||
| 节点管理 | /nodes_list | 注册A/B节点(SSH信息) |
|
||||
| 数据库同步 | /sync_databases_list | MySQL主从复制配置 |
|
||||
| 文件同步 | /sync_directories_list | rsync目录同步配置 |
|
||||
- dbbackup — 数据库备份 + 日志同步(binlog 复制)
|
||||
- filesync — 运行文件同步(rsync)
|
||||
- hostwatch — 日志监控 + 日志分析 + 主机指标
|
||||
|
||||
## Agent部署
|
||||
|
||||
在主机A和备机B上:
|
||||
|
||||
```bash
|
||||
# 配置环境变量
|
||||
export FOMS_MANAGEMENT_URL=http://C的IP:9080
|
||||
export FOMS_NODE_ID=<节点ID>
|
||||
export FOMS_AGENT_TOKEN=<安全令牌>
|
||||
|
||||
# 运行Agent
|
||||
python3 scripts/foms-agent.py
|
||||
```
|
||||
仪表盘对这三模块的表做同库只读聚合(松耦合)。
|
||||
|
||||
38
app/foms.py
38
app/foms.py
@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""FOMS — Failover Management System 主入口"""
|
||||
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)
|
||||
@ -1,12 +0,0 @@
|
||||
"""FOMS 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
|
||||
78
build.sh
78
build.sh
@ -1,78 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
cdir=$(pwd)
|
||||
echo "=== FOMS 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 核心
|
||||
cd $cdir && $cdir/py3/bin/pip install -e .
|
||||
|
||||
# 5.5 Install 业务模块(dbbackup / filesync / hostwatch)
|
||||
for m in 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 FOMS 核心 DDL
|
||||
cd $cdir
|
||||
[ -d models ] && json2ddl mysql models/ > $cdir/models/mysql.ddl.sql 2>/dev/null || echo " skip ddl"
|
||||
|
||||
# 7. Generate FOMS 核心 CRUD UI (xls2ui for json/)
|
||||
cd $cdir
|
||||
if [ -d json ]; then
|
||||
cd json
|
||||
for f in *.json; do
|
||||
echo " xls2ui foms $f"
|
||||
xls2ui -m ../models -o ../wwwroot foms "$f" 2>/dev/null || echo " skipped"
|
||||
done
|
||||
fi
|
||||
|
||||
# 7.5 生成模块 CRUD + 软链模块 wwwroot → 主 wwwroot/<模块名>
|
||||
cd $cdir
|
||||
for m in dbbackup filesync hostwatch; do
|
||||
md=$cdir/pkgs/$m
|
||||
# 生成模块 CRUD 页面
|
||||
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
|
||||
# 软链模块 wwwroot → 主 wwwroot/<模块名>(URL 前缀 /dbbackup/ 等)
|
||||
ln -sfn "$md/wwwroot" "$cdir/wwwroot/$m"
|
||||
done
|
||||
|
||||
# 8. Create runtime dirs
|
||||
mkdir -p $cdir/logs $cdir/files
|
||||
|
||||
echo "=== FOMS Build Complete ==="
|
||||
@ -1,42 +0,0 @@
|
||||
{
|
||||
"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]$/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"]
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,26 @@
|
||||
"""foms — 主备切换核心业务模块(FOMS 的领域核心)。
|
||||
|
||||
独立可安装模块,由纯伞仓 foms_app 组装。
|
||||
|
||||
内容:
|
||||
· 表:clusters、nodes、switchover_logs、switchover_steps、node_commands
|
||||
· 集群/节点 CRUD、Agent 心跳、仪表盘、远程命令队列
|
||||
· 切换编排器(六步状态机 + 逆向回滚 + 防脑裂,见 failover.py)
|
||||
· 健康判定器(后台循环 + 防抖 + 半自动告警)
|
||||
· scripts/foms-agent.py:被管主机守护进程(非本包代码,独立部署)
|
||||
|
||||
邻接能力已拆为独立模块(不在本包内):
|
||||
· dbbackup(数据库备份 + 日志同步)、filesync(文件同步)、hostwatch(日志监控分析)
|
||||
"""
|
||||
from .init import (
|
||||
load_foms,
|
||||
create_cluster, update_cluster, delete_cluster,
|
||||
create_node, update_node, delete_node,
|
||||
create_sync_db, update_sync_db, delete_sync_db,
|
||||
create_sync_dir, update_sync_dir, delete_sync_dir,
|
||||
agent_heartbeat, agent_report_health,
|
||||
agent_heartbeat,
|
||||
trigger_switchover, trigger_switchback,
|
||||
get_dashboard_stats,
|
||||
agent_report_metrics, agent_push_logs,
|
||||
get_node_detail, get_node_metrics_history,
|
||||
exec_remote_command, agent_poll_commands, agent_report_command_result,
|
||||
)
|
||||
from .failover import (
|
||||
confirm_switchover, run_switchover, health_judge_loop, register_failover,
|
||||
)
|
||||
|
||||
171
init/data.yaml
171
init/data.yaml
@ -1,171 +0,0 @@
|
||||
appcodes:
|
||||
- id: cluster_status
|
||||
name: 集群状态
|
||||
hierarchy_flg: 0
|
||||
- id: node_role
|
||||
name: 节点角色
|
||||
hierarchy_flg: 0
|
||||
- id: node_status
|
||||
name: 节点状态
|
||||
hierarchy_flg: 0
|
||||
- id: sync_status
|
||||
name: 同步状态
|
||||
hierarchy_flg: 0
|
||||
- id: sync_result
|
||||
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: 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: 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_trigger_auto
|
||||
parentid: switchover_trigger
|
||||
k: auto
|
||||
v: 自动检测
|
||||
- id: switchover_trigger_manual
|
||||
parentid: switchover_trigger
|
||||
k: manual
|
||||
v: 手动触发
|
||||
|
||||
- id: switchover_status_pending
|
||||
parentid: switchover_status
|
||||
k: pending
|
||||
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_failed
|
||||
parentid: switchover_status
|
||||
k: 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: 严重
|
||||
@ -1,155 +0,0 @@
|
||||
#!/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())
|
||||
@ -1,97 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""FOMS RBAC permission registration"""
|
||||
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 = [
|
||||
"/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/sync_databases_create.dspy",
|
||||
"/api/sync_databases_update.dspy",
|
||||
"/api/sync_databases_delete.dspy",
|
||||
"/api/sync_directories_create.dspy",
|
||||
"/api/sync_directories_update.dspy",
|
||||
"/api/sync_directories_delete.dspy",
|
||||
"/api/agent_heartbeat.dspy",
|
||||
"/api/agent_report_health.dspy",
|
||||
"/api/trigger_switchover.dspy",
|
||||
"/api/trigger_switchback.dspy",
|
||||
"/api/dashboard_stats.dspy",
|
||||
# CRUD list pages
|
||||
"/clusters_list",
|
||||
"/clusters_list/index.ui",
|
||||
"/nodes_list",
|
||||
"/nodes_list/index.ui",
|
||||
"/sync_databases_list",
|
||||
"/sync_databases_list/index.ui",
|
||||
"/sync_directories_list",
|
||||
"/sync_directories_list/index.ui",
|
||||
# CRUD generated files
|
||||
"/clusters_list/get_clusters_list.dspy",
|
||||
"/clusters_list/add_clusters_list.dspy",
|
||||
"/clusters_list/update_clusters_list.dspy",
|
||||
"/clusters_list/delete_clusters_list.dspy",
|
||||
"/nodes_list/get_nodes_list.dspy",
|
||||
"/nodes_list/add_nodes_list.dspy",
|
||||
"/nodes_list/update_nodes_list.dspy",
|
||||
"/nodes_list/delete_nodes_list.dspy",
|
||||
"/sync_databases_list/get_sync_databases_list.dspy",
|
||||
"/sync_databases_list/add_sync_databases_list.dspy",
|
||||
"/sync_databases_list/update_sync_databases_list.dspy",
|
||||
"/sync_databases_list/delete_sync_databases_list.dspy",
|
||||
"/sync_directories_list/get_sync_directories_list.dspy",
|
||||
"/sync_directories_list/add_sync_directories_list.dspy",
|
||||
"/sync_directories_list/update_sync_directories_list.dspy",
|
||||
"/sync_directories_list/delete_sync_directories_list.dspy",
|
||||
# Metrics & logs
|
||||
"/api/agent_report_metrics.dspy",
|
||||
"/api/agent_push_logs.dspy",
|
||||
"/api/node_detail.dspy",
|
||||
"/api/node_metrics_history.dspy",
|
||||
# Remote command
|
||||
"/api/exec_remote_command.dspy",
|
||||
"/api/agent_poll_commands.dspy",
|
||||
"/api/agent_report_command_result.dspy",
|
||||
]
|
||||
|
||||
# Agent heartbeat endpoint: no auth (agents use token)
|
||||
PATHS_ANY.append("/api/agent_heartbeat.dspy")
|
||||
PATHS_ANY.append("/api/agent_report_health.dspy")
|
||||
PATHS_ANY.append("/api/agent_report_metrics.dspy")
|
||||
PATHS_ANY.append("/api/agent_push_logs.dspy")
|
||||
PATHS_ANY.append("/api/agent_poll_commands.dspy")
|
||||
PATHS_ANY.append("/api/agent_report_command_result.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 to apply.")
|
||||
6
start.sh
6
start.sh
@ -1,6 +0,0 @@
|
||||
#!/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 started (PID: $(cat foms.pid)) on port ${FOMS_PORT:-9080}"
|
||||
9
stop.sh
9
stop.sh
@ -1,9 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
cd "$(dirname "$0")"
|
||||
if [ -f foms.pid ]; then
|
||||
pid=$(cat foms.pid)
|
||||
kill $pid 2>/dev/null && echo "FOMS stopped (PID: $pid)" || echo "FOMS not running"
|
||||
rm -f foms.pid
|
||||
else
|
||||
echo "No PID file found"
|
||||
fi
|
||||
@ -1,87 +0,0 @@
|
||||
{
|
||||
"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('sync_databases_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('sync_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",
|
||||
"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"}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user