feat(confs): integrate dnfs service with centralized config management

- Add dnfs.ini to confs/configs/ with server, database, ai_review, confs sections
- Refactor dnfs.py to use three-layer fail-safe config loading (defaults → confs → env vars)
- Unify timeouts for DB RPC and AI review calls via configuration
- Add _merge_ini() whitelist merging to prevent unexpected config injection
- Environment variables have highest priority for Docker networking (db/ai_review hostnames)
- Document config loading mechanism in both confs/README.md and dnfs/README.md

Follows the same pattern as static service integration; confs failure falls back
gracefully to built-in defaults + environment variables.
This commit is contained in:
qing committed 2026-08-01 04:13:39 +08:00
1 parent b4436b192a
commit 4e190953c0
4 files changed
+140 -31

No files matched your search

+9 -2
View File
@@ -236,6 +236,7 @@ except RpcError as e:
| 服务 | 配置文件 | 接入方式 | | 服务 | 配置文件 | 接入方式 |
|------|---------|---------| |------|---------|---------|
| [static](../static/) | [static.ini](configs/static.ini) | 三层 fail-safe:内置默认值 → confs(优先)→ 本地 `config.ini` 回退 | | [static](../static/) | [static.ini](configs/static.ini) | 三层 fail-safe:内置默认值 → confs(优先)→ 本地 `config.ini` 回退 |
| [dnfs](../dnfs/) | [dnfs.ini](configs/dnfs.ini) | 三层 fail-safe:内置默认值 → confs(优先)→ 环境变量覆盖 |
static 的接入实现见 [static/fsys.py](../static/fsys.py): static 的接入实现见 [static/fsys.py](../static/fsys.py):
- `_fetch_from_confs()` 通过 `RpcClient` 调 `get` 路由取 `static.ini`,3 秒超时 - `_fetch_from_confs()` 通过 `RpcClient` 调 `get` 路由取 `static.ini`,3 秒超时
@@ -243,7 +244,12 @@ static 的接入实现见 [static/fsys.py](../static/fsys.py):
- `_merge_ini()` 仅覆盖已知的 section/key(白名单合并,不引入未预期字段) - `_merge_ini()` 仅覆盖已知的 section/key(白名单合并,不引入未预期字段)
- 配置来源会记录到日志(`logs.info/warning`),便于排查实际用了哪一层 - 配置来源会记录到日志(`logs.info/warning`),便于排查实际用了哪一层
其他服务(gateway / waf / ai_review / smtp / dnfs / db)尚未接入,仍读各自本地配置。 dnfs 的接入实现见 [dnfs/dnfs.py](../dnfs/dnfs.py):
- 三层加载顺序:内置默认值 → confs(dnfs.ini)→ 环境变量覆盖(优先级最高)
- 支持的环境变量:`LISTEN_ADDR`/`LISTEN_PORT`/`DB_HOST`/`DB_PORT`/`REVIEW_URL`/`CONFS_HOST`/`CONFS_PORT`
- Docker 环境通过环境变量传入服务名(db/ai_review),覆盖 confs 中的 127.0.0.1 默认值
其他服务(gateway / waf / ai_review / smtp / db)尚未接入,仍读各自本地配置。
## 项目结构 ## 项目结构
@@ -253,7 +259,8 @@ confs/
├── log_client.py # 统一日志客户端 SDK(与其他服务一致的副本) ├── log_client.py # 统一日志客户端 SDK(与其他服务一致的副本)
├── configs/ # 配置文件存放目录(被分发对象) ├── configs/ # 配置文件存放目录(被分发对象)
│ ├── example.ini # 示例配置 │ ├── example.ini # 示例配置
│ └── static.ini # static 服务配置(已接入分发) │ ├── static.ini # static 服务配置(已接入分发)
│ └── dnfs.ini # dnfs 服务配置(已接入分发)
├── docs/ ├── docs/
│ └── SECURITY_AUDIT_20260801.md # 安全审计报告 │ └── SECURITY_AUDIT_20260801.md # 安全审计报告
├── _set.sh # 初始化脚本(建 venv + 安装依赖) ├── _set.sh # 初始化脚本(建 venv + 安装依赖)
+16
View File
@@ -0,0 +1,16 @@
[server]
host = 127.0.0.1
port = 8006
[database]
host = 127.0.0.1
port = 8005
timeout = 10
[ai_review]
url = http://127.0.0.1:8007/review
timeout = 10
[confs]
host = 127.0.0.1
port = 8010
+33 -7
View File
@@ -196,15 +196,41 @@ curl -X POST http://localhost:8006/update_note \
## 配置 ## 配置
服务地址与上游依赖硬编码在 [dnfs.py](dnfs.py): dnfs 已接入 confs 统一配置服务,采用**三层 fail-safe** 加载模式(与 static 服务一致):
| 常量 | 值 | 说明 | **加载顺序**:内置默认值 → confs 统一配置服务(dnfs.ini,优先)→ 环境变量覆盖(最高优先级)
|------|----|------|
| `ADDR` / `PORT` | `127.0.0.1` / `8006` | 监听地址 |
| `DB_SERVER_URL` | `http://127.0.0.1:8005/execute` | db 服务 |
| `REVIEW_SERVER_URL` | `http://127.0.0.1:8007/review` | ai_review 服务 |
调用超时均为 10 秒。 ### 配置文件
集中托管在 [confs/configs/dnfs.ini](../confs/configs/dnfs.ini),由 confs 服务分发:
| Section | Key | 默认值 | 说明 |
|---------|-----|--------|------|
| `[server]` | `host` | `127.0.0.1` | 监听地址 |
| `[server]` | `port` | `8006` | 监听端口 |
| `[database]` | `host` | `127.0.0.1` | db 服务地址 |
| `[database]` | `port` | `8005` | db 服务端口 |
| `[database]` | `timeout` | `10` | RPC 调用超时(秒) |
| `[ai_review]` | `url` | `http://127.0.0.1:8007/review` | AI 审核接口地址 |
| `[ai_review]` | `timeout` | `10` | AI 审核请求超时(秒) |
| `[confs]` | `host` | `127.0.0.1` | confs 服务地址 |
| `[confs]` | `port` | `8010` | confs 服务端口 |
### 环境变量(Docker 环境必需,优先级最高)
环境变量覆盖配置文件值,容器化部署时通过 docker-compose.yml 传入服务名:
| 变量 | 容器内值 | 说明 |
|------|---------|------|
| `LISTEN_ADDR` | `0.0.0.0` | 监听地址(容器内需绑定所有网卡) |
| `LISTEN_PORT` | `8006` | 监听端口 |
| `DB_HOST` | `db` | db 服务主机名(docker-compose 服务名) |
| `DB_PORT` | `8005` | db 服务端口 |
| `REVIEW_URL` | `http://ai_review:8007/review` | ai_review 服务地址 |
| `CONFS_HOST` | - | 可选,confs 容器化后使用 |
| `CONFS_PORT` | - | 可选,confs 容器化后使用 |
配置来源会记录到日志(`logs.info/warning`),便于排查实际用了哪一层。本地裸机运行时 confs 不可用会自动回退到内置默认值 + 环境变量。
## 前端 ## 前端
+82 -22
View File
@@ -1,9 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
''' '''
Dendrachy Network File-System (dnfs) Dendrachy Network File-System (dnfs)
@@ -21,8 +17,10 @@ from flask_cors import CORS
#from flask_sqlalchemy import SQLAlchemy # 使用pymsg模块进行通信 #from flask_sqlalchemy import SQLAlchemy # 使用pymsg模块进行通信
from waitress import serve from waitress import serve
import os import os
import sys
import json import json
import base64 import base64
import configparser
import urllib.request import urllib.request
import urllib.error import urllib.error
from typing import Dict, Any from typing import Dict, Any
@@ -32,14 +30,75 @@ from log_client import logs
# pymsg 导入 # pymsg 导入
# 我们有在依赖列表中加入pymsg # 我们有在依赖列表中加入pymsg
# 这个可以省略也许 # 这个可以省略也许
import sys as _sys
_sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) _sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from pymsg import RpcClient, RpcError # noqa: E402 from pymsg import RpcClient, RpcError # noqa: E402
# 从环境变量中获取参数 # ==================== 配置加载(三层 fail-safe) ====================
# 随后我们专门做一个统一配置微服务 # 加载顺序:内置默认值 → confs 统一配置服务(dnfs.ini) → 环境变量覆盖
_DBV2_ADDR = os.environ.get('DB_HOST', '127.0.0.1') # 环境变量优先级最高(Docker 环境使用服务名 db/ai_review 而非 127.0.0.1)
_DBV2_PORT = int(os.environ.get('DB_PORT', '8005'))
_dnfs_config = {
'server': {'host': '127.0.0.1', 'port': '8006'},
'database': {'host': '127.0.0.1', 'port': '8005', 'timeout': '10'},
'ai_review': {'url': 'http://127.0.0.1:8007/review', 'timeout': '10'},
'confs': {'host': '127.0.0.1', 'port': '8010'},
}
def _merge_ini(text):
"""把 ini 文本合并进 _dnfs_config(仅覆盖已知的 section/key,白名单合并)。"""
ini = configparser.ConfigParser(interpolation=None)
ini.read_string(text)
for section in _dnfs_config:
if ini.has_section(section):
for key, value in ini[section].items():
if key in _dnfs_config[section]:
_dnfs_config[section][key] = value
def _fetch_from_confs():
"""从 confs 统一配置服务拉取 dnfs.ini 全文,失败返回 None(fail-safe)。"""
try:
host = os.environ.get('CONFS_HOST', _dnfs_config['confs']['host'])
port = int(os.environ.get('CONFS_PORT', _dnfs_config['confs']['port']))
data = RpcClient(host, port, 'dnfs', timeout=3).call(
'get', {'name': 'dnfs.ini'})
return data.get('content')
except Exception:
return None
_confs_text = _fetch_from_confs()
if _confs_text is not None:
_merge_ini(_confs_text)
logs.info("dnfs", "配置来源: confs 统一配置服务 (dnfs.ini)")
else:
logs.warning("dnfs", "confs 不可用,使用内置默认值 + 环境变量")
# 环境变量覆盖(优先级最高,Docker 环境需要)
if os.environ.get('LISTEN_ADDR'):
_dnfs_config['server']['host'] = os.environ['LISTEN_ADDR']
if os.environ.get('LISTEN_PORT'):
_dnfs_config['server']['port'] = os.environ['LISTEN_PORT']
if os.environ.get('DB_HOST'):
_dnfs_config['database']['host'] = os.environ['DB_HOST']
if os.environ.get('DB_PORT'):
_dnfs_config['database']['port'] = os.environ['DB_PORT']
if os.environ.get('REVIEW_URL'):
_dnfs_config['ai_review']['url'] = os.environ['REVIEW_URL']
if os.environ.get('CONFS_HOST'):
_dnfs_config['confs']['host'] = os.environ['CONFS_HOST']
if os.environ.get('CONFS_PORT'):
_dnfs_config['confs']['port'] = os.environ['CONFS_PORT']
# 提取配置常量
_DBV2_ADDR = _dnfs_config['database']['host']
_DBV2_PORT = int(_dnfs_config['database']['port'])
_DB_TIMEOUT = int(_dnfs_config['database']['timeout'])
_REVIEW_SERVER_URL = _dnfs_config['ai_review']['url']
_REVIEW_TIMEOUT = int(_dnfs_config['ai_review']['timeout'])
_ADDR = _dnfs_config['server']['host']
_PORT = int(_dnfs_config['server']['port'])
# 管理员判定已上移至 gateway:登录时命中 [Admin] users 白名单即在 Token payload # 管理员判定已上移至 gateway:登录时命中 [Admin] users 白名单即在 Token payload
# 写入 is_admin=true,本服务通过 info.get('is_admin') 读取,不再本地维护名单。 # 写入 is_admin=true,本服务通过 info.get('is_admin') 读取,不再本地维护名单。
@@ -48,13 +107,15 @@ _DBV2_PORT = int(os.environ.get('DB_PORT', '8005'))
_rpc = RpcClient(_DBV2_ADDR, _DBV2_PORT, "dnfs") _rpc = RpcClient(_DBV2_ADDR, _DBV2_PORT, "dnfs")
def _call_dbv2(payload: dict, timeout: int = 10) -> dict: def _call_dbv2(payload: dict, timeout: int = None) -> dict:
"""通过 pymsg RPC 调用 dbv2 数据库服务(自动注入 ``_route: "execute"``)。 """通过 pymsg RPC 调用 dbv2 数据库服务(自动注入 ``_route: "execute"``)。
返回完整响应 dict(``{"success": ..., "data"/"error": ...}``), 返回完整响应 dict(``{"success": ..., "data"/"error": ...}``),
与历史行为一致,调用方无需改动。服务端业务错误返回 success=False; 与历史行为一致,调用方无需改动。服务端业务错误返回 success=False;
网络层故障抛 ConnectionError。 网络层故障抛 ConnectionError。
""" """
if timeout is None:
timeout = _DB_TIMEOUT
try: try:
data = _rpc.call("execute", payload, timeout=timeout) data = _rpc.call("execute", payload, timeout=timeout)
return {"success": True, "data": data} return {"success": True, "data": data}
@@ -65,15 +126,14 @@ def _call_dbv2(payload: dict, timeout: int = 10) -> dict:
f"dbv2 调用失败 ({_DBV2_ADDR}:{_DBV2_PORT}): {e}" f"dbv2 调用失败 ({_DBV2_ADDR}:{_DBV2_PORT}): {e}"
) from None ) from None
# AI审核服务地址,从环境变量获取 # AI审核服务地址
REVIEW_SERVER_URL = os.environ.get('REVIEW_URL', 'http://127.0.0.1:8007/review') def review_content(content: str, timeout: int = None) -> Dict[str, Any]:
def review_content(content: str, timeout: int = 10) -> Dict[str, Any]:
""" """
调用 AI 内容审核服务的 /review 接口,对给定文本进行审核。 调用 AI 内容审核服务的 /review 接口,对给定文本进行审核。
Args: Args:
content: 待审核的文本内容,不可为空。 content: 待审核的文本内容,不可为空。
timeout: 请求超时时间(秒),默认 10 秒。 timeout: 请求超时时间(秒),默认使用配置值。
Returns: Returns:
字典,包含接口返回的字段,例如: 字典,包含接口返回的字段,例如:
@@ -83,7 +143,9 @@ def review_content(content: str, timeout: int = 10) -> Dict[str, Any]:
- 请求参数错误时:{"error": "content is required"} 等 - 请求参数错误时:{"error": "content is required"} 等
- 网络或异常时:{"error": "request_failed", "detail": "..."} - 网络或异常时:{"error": "request_failed", "detail": "..."}
""" """
url = REVIEW_SERVER_URL if timeout is None:
timeout = _REVIEW_TIMEOUT
url = _REVIEW_SERVER_URL
payload = {"content": content} payload = {"content": content}
json_data = json.dumps(payload).encode("utf-8") json_data = json.dumps(payload).encode("utf-8")
@@ -192,7 +254,7 @@ def add_note():
# 内容审查 # 内容审查
review = review_content(title + '\n' + content, 10) review = review_content(title + '\n' + content)
if True != review: raise DnfsExp('内容审核不通过,用户'+info.get('user')+',从'+request.remote_addr, 400) if True != review: raise DnfsExp('内容审核不通过,用户'+info.get('user')+',从'+request.remote_addr, 400)
# 执行插入 # 执行插入
@@ -412,7 +474,7 @@ def update_note():
return jsonify({"error": 'It\'s not your note'}), 403 return jsonify({"error": 'It\'s not your note'}), 403
# 内容审查 # 内容审查
review = review_content(title + '\n' + content, 10) review = review_content(title + '\n' + content)
if True != review: if True != review:
#return jsonify({'error': 'review failed', 'data': review}), 400 #return jsonify({'error': 'review failed', 'data': review}), 400
logs.warning('dnfs', f'update_note 内容审核未通过 id={id} title={title}', extra={'user': info['user']}) logs.warning('dnfs', f'update_note 内容审核未通过 id={id} title={title}', extra={'user': info['user']})
@@ -502,9 +564,7 @@ def get_note():
# """返回应用主页""" # """返回应用主页"""
# return render_template('index.html') # return render_template('index.html')
ADDR = os.environ.get('LISTEN_ADDR', '0.0.0.0')
PORT = int(os.environ.get('LISTEN_PORT', '8006'))
if __name__ == "__main__": if __name__ == "__main__":
logs.info('dnfs', f'服务启动,监听 http://{ADDR}:{PORT}') logs.info('dnfs', f'服务启动,监听 http://{_ADDR}:{_PORT}(db={_DBV2_ADDR}:{_DBV2_PORT}, ai_review={_REVIEW_SERVER_URL})')
print(f'Listen at http://{ADDR}:{PORT}') print(f'Listen at http://{_ADDR}:{_PORT}')
serve(app, host=ADDR, port=PORT) serve(app, host=_ADDR, port=_PORT)