feat: foms_app 纯伞(应用壳)— 组装与启动,无业务逻辑
业务全部在独立模块仓库(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 侧已删除。
This commit is contained in:
commit
594f02a49d
8
.env.example
Normal file
8
.env.example
Normal file
@ -0,0 +1,8 @@
|
||||
# FOMS Environment Variables
|
||||
export FOMS_PORT=9080
|
||||
|
||||
# Database
|
||||
export MYSQL_ROOT_PASSWORD=
|
||||
|
||||
# Agent Communication
|
||||
export FOMS_AGENT_TOKEN=
|
||||
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@ -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
|
||||
51
README.md
Normal file
51
README.md
Normal file
@ -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
|
||||
```
|
||||
45
app/foms.py
Normal file
45
app/foms.py
Normal file
@ -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)
|
||||
12
app/global_func.py
Normal file
12
app/global_func.py
Normal file
@ -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
|
||||
62
build.sh
Executable file
62
build.sh
Executable file
@ -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 ==="
|
||||
46
conf/config.json
Normal file
46
conf/config.json
Normal file
@ -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"]
|
||||
]
|
||||
}
|
||||
}
|
||||
360
init/data.yaml
Normal file
360
init/data.yaml
Normal file
@ -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: 严重
|
||||
155
scripts/init_rbac.py
Executable file
155
scripts/init_rbac.py
Executable file
@ -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())
|
||||
93
scripts/load_path.py
Executable file
93
scripts/load_path.py
Executable file
@ -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.")
|
||||
6
start.sh
Executable file
6
start.sh
Executable file
@ -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 </dev/null > $PWD/logs/foms.out 2>&1 &
|
||||
echo $! > foms.pid
|
||||
echo "FOMS_APP started (PID: $(cat foms.pid)) on port ${FOMS_PORT:-9080}"
|
||||
9
stop.sh
Executable file
9
stop.sh
Executable file
@ -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
|
||||
96
wwwroot/index.ui
Normal file
96
wwwroot/index.ui
Normal file
@ -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"}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user