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
|
*.pid
|
||||||
logs/
|
logs/
|
||||||
files/
|
files/
|
||||||
pkgs/
|
models/mysql.ddl.sql
|
||||||
bricks/
|
.DS_Store
|
||||||
py3/
|
|
||||||
.env
|
.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)
|
precheck → fence → promote → flip_sync → verify → converge
|
||||||
│
|
|
||||||
├── 主机 A ←──MySQL复制/rsync──→ 备机 B
|
|
||||||
│
|
|
||||||
└── Agent 守护进程 (部署在 A & B)
|
|
||||||
```
|
```
|
||||||
|
失败逆向回滚;防脑裂不变量:fence 先于 promote,fence 失败硬中止。
|
||||||
|
详见 `docs/deploy-operations.md`。
|
||||||
|
|
||||||
## 快速开始
|
## 安装
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 构建
|
pip install -e .
|
||||||
./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
|
|
||||||
```
|
```
|
||||||
|
由 foms_app 的 build.sh 统一组装。`load_foms()` 时自动注册切换编排器
|
||||||
|
并拉起健康判定器后台任务(幂等)。
|
||||||
|
|
||||||
## 模块
|
## 邻接模块(独立仓库)
|
||||||
|
|
||||||
| 模块 | 路径 | 说明 |
|
- dbbackup — 数据库备份 + 日志同步(binlog 复制)
|
||||||
|------|------|------|
|
- filesync — 运行文件同步(rsync)
|
||||||
| 集群管理 | /clusters_list | 配置主备集群 |
|
- hostwatch — 日志监控 + 日志分析 + 主机指标
|
||||||
| 节点管理 | /nodes_list | 注册A/B节点(SSH信息) |
|
|
||||||
| 数据库同步 | /sync_databases_list | MySQL主从复制配置 |
|
|
||||||
| 文件同步 | /sync_directories_list | rsync目录同步配置 |
|
|
||||||
|
|
||||||
## 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 (
|
from .init import (
|
||||||
|
load_foms,
|
||||||
create_cluster, update_cluster, delete_cluster,
|
create_cluster, update_cluster, delete_cluster,
|
||||||
create_node, update_node, delete_node,
|
create_node, update_node, delete_node,
|
||||||
create_sync_db, update_sync_db, delete_sync_db,
|
agent_heartbeat,
|
||||||
create_sync_dir, update_sync_dir, delete_sync_dir,
|
|
||||||
agent_heartbeat, agent_report_health,
|
|
||||||
trigger_switchover, trigger_switchback,
|
trigger_switchover, trigger_switchback,
|
||||||
get_dashboard_stats,
|
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,
|
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