mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:17:47 +08:00
feat(ql3): stage legacy data directory
This commit is contained in:
@@ -11,6 +11,25 @@
|
||||
|
||||
最新增量证据(2026-08-21):
|
||||
|
||||
- D-385/ADR-0478(已接受):把 D-384 只读 data directory plan 推进为产品级私有 stage/verify。既有一次性 `ql3-adoption`
|
||||
新增 exact `local-data-directory.adoption.stage|verify`;`stagingRoot` 必须是私有 deployment root 内、legacy data root 外的
|
||||
no-replace 路径。stage 只把 `scripts/upload` 放入 `copy-reviewed`,把 `config/db/ssh.d` 放入 `transform-input`,排除
|
||||
`db/database.sqlite` 及 sidecar;日志/备份保持外部,repo/raw/dep_cache/deps 在目标重建。复制与 verify 均拒绝 link、特殊文件、
|
||||
owner/mode 漂移和额外条目,目录/文件统一 `0700/0600`,使用 64 KiB 缓冲,并在复制循环再次执行 Edge/Standalone 条目、总字节、
|
||||
单文件和深度预算。目录计划必须为 reviewable、恰有一个主库且无 active sidecar;SQLite source 精确绑定
|
||||
`<dataRoot>/db/database.sqlite`,命令复用 D-383 activation acquisition,在复制/校验期间持有 source write fence,并把 activation
|
||||
与 SQLite adoption manifest digest 写入内容无关目录清单。`.incomplete` 在创建根后先持久化,payload/manifest 完成后才移除;
|
||||
crash residue、目标/源漂移和扩权 command 全部失败关闭。能力继续内聚在 `lifecycle/data-directory-adoption/`,按
|
||||
`contract/command/staging/filesystem/manifest/inventory` 职责组织;最终 orchestration 为 319 行,安全文件系统原语与 manifest
|
||||
验证分别为 430/437 行,没有再拆 workspace package。整体不新增 dependency、binary 或常驻对象;workspace 仍为 18 packages,
|
||||
Local Owner 为 `122 source / 121 nested / 1 root binary entry`。D-385 focused inspect/stage/verify `10/10`,Local Owner
|
||||
`200 total / 195 pass / 5 conditional skip / 0 fail`;backend `1,535 total / 1,533 pass / 2 conditional skip / 0 fail`,
|
||||
`pnpm build:back` 与 18-package clean build/逐包测试通过。八项架构审计和按顺序执行的 14 档 artifact audit 全 compatible;基础
|
||||
Edge/Standalone `2,598,669 / 2,598,747` bytes、316 files、57 modules,Adopted `2,818,404 / 2,818,527` bytes、336 files、
|
||||
58 modules,Application+AI `4,502,262 / 4,502,394` bytes、511 files、141 modules,MCP
|
||||
`7,324,601 / 7,324,709` bytes、802 files、227 modules,证明一次性 adoption authority 未进入常驻制品。本切片不改变
|
||||
PostgreSQL 语义,因此不重新占有 HA 证明。config/Keyv/SSH 目标转换、固定物理 Edge 的 RSS/I/O/ENOSPC/断电门及
|
||||
systemd/OpenRC/Compose lineage 留给 D-386 以后完成。
|
||||
- D-384/ADR-0477(已接受):完整 2.x data directory 接管先落地为一次性、只读、有界 inventory,而不是直接复用 legacy shell `tar`
|
||||
或盲拷整个目录。既有 `ql3-adoption` 新增 exact 私有命令 `local-data-directory.adoption.inspect`,固定分类
|
||||
`config/db/ssh.d→transform`、`scripts/upload→copy_reviewed`、`log/syslog/bak→retain_external(root-only)`、
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# ADR-0478:私有 Legacy Data Directory 暂存与稳定校验
|
||||
|
||||
- 状态:Accepted
|
||||
- 日期:2026-08-21
|
||||
- 关联:QL-RFC-0001、ADR-0476、ADR-0477
|
||||
|
||||
## 上下文
|
||||
|
||||
ADR-0477 已把完整 QingLong 2.x `data` 目录收敛为确定性、有界、内容无关的接管计划,但只读计划不是可恢复副本。下一步必须把
|
||||
审核过的 `scripts/upload` 和待转换的 `config/db/ssh.d` 放入私有暂存区,同时继续排除历史日志、备份、仓库 checkout、原始缓存和
|
||||
跨架构依赖缓存。
|
||||
|
||||
目录暂存不能脱离 ADR-0476 的主 SQLite 接管独立成立。否则 operator 可能把一版目录计划与另一版主库 target/activation 混合,或在
|
||||
2.x 主库仍可写时生成看似完整但跨资产不一致的副本。低配路由设备还要求复制过程使用固定内存、硬容量预算并在崩溃后留下可识别的
|
||||
不完整状态,而不是把整个目录先读入内存或交给无边界 `tar`。
|
||||
|
||||
## 决策
|
||||
|
||||
### 1. 在既有领域目录扩展 stage/verify
|
||||
|
||||
`@qinglong/local-owner-cli` 的 `lifecycle/data-directory-adoption/` 增加两个 exact operation:
|
||||
|
||||
- `local-data-directory.adoption.stage`;
|
||||
- `local-data-directory.adoption.verify`。
|
||||
|
||||
两者继续使用既有一次性 `ql3-adoption` 私有 command-file 入口。实现由同一领域目录中的 `contract`、`inventory`、`staging` 和产品
|
||||
`command` 组合,不新增 workspace package、第三方依赖、binary、daemon、listener、watcher、timer、数据库连接或部署对象。
|
||||
|
||||
### 2. 使用目录计划与 SQLite activation 双围栏
|
||||
|
||||
stage 必须提交精确 `expectedPlanDigest`,verify 必须提交精确 `expectedManifestDigest`。两者还必须提交完整且 exact 的 SQLite binding:
|
||||
|
||||
- `sourcePath`、`targetPath`、`recoveryPath`、`manifestPath`、`activationPath`;
|
||||
- `expectedActivationDigest`。
|
||||
|
||||
SQLite source 必须严格等于 `<dataRoot>/db/database.sqlite`,其余 SQLite adoption 证据必须位于 `dataRoot` 外。命令复用
|
||||
`@qinglong/local-admin/runtime` 的 `acquireLocalSqliteActivation`,重新验证 adoption manifest、target identity、source snapshot 与
|
||||
activation digest,并在目录复制/静态校验期间持有 source `BEGIN IMMEDIATE` 写栅栏。Profile、SQLite activation digest 和 SQLite
|
||||
adoption manifest digest 都进入目录清单。
|
||||
|
||||
### 3. 固定、私有、no-replace 暂存布局
|
||||
|
||||
`stagingRoot` 必须是 `0700` canonical `deploymentRoot` 内的不存在路径,且不能位于 `dataRoot` 内。stage 使用 no-replace 创建它,并只产生:
|
||||
|
||||
```text
|
||||
stagingRoot/
|
||||
manifest.json 0600
|
||||
payload/
|
||||
copy-reviewed/ 0700
|
||||
scripts/...
|
||||
upload/...
|
||||
transform-input/ 0700
|
||||
config/...
|
||||
db/... # 不含 database.sqlite 及其 sidecar
|
||||
ssh.d/...
|
||||
```
|
||||
|
||||
目录统一为 `0700`,文件统一为 `0600`。`log/syslog/bak` 保持外部,`repo/raw/dep_cache/deps` 在目标重新生成。主 SQLite 不进入目录
|
||||
payload,因为 ADR-0476 的 recovery/target/activation 已是它的独立恢复权威。
|
||||
|
||||
### 4. 固定内存复制、重复预算与稳定身份
|
||||
|
||||
复制仅接受当前 UID 拥有、group/world 不可写、非 symlink 的目录和普通单链接文件。源文件通过 `O_NOFOLLOW` descriptor 和 64 KiB
|
||||
缓冲流式复制,打开前后必须保持 device、inode、mode、link count、UID、size、mtime 和 ctime。源目录遍历前后也必须稳定。
|
||||
|
||||
复制循环独立重复执行 ADR-0477 的 Profile 条目数、总字节、单文件和深度预算;不能只依赖较早的 inspect。复制完成并释放 SQLite
|
||||
栅栏后,再重新生成完整目录计划,必须与审核计划逐字段一致。目标 verify 同样按固定顺序、固定内存重新哈希,拒绝额外条目、缺失项、
|
||||
symlink、硬链接、特殊文件、错误 owner 和非私有 mode。
|
||||
|
||||
### 5. 显式崩溃残留与内容无关清单
|
||||
|
||||
stage 创建根后立即以 no-replace 写入并持久化固定 `.incomplete` 标记;复制或清单发布前后的任何失败都不覆盖、不自动重用该目录。
|
||||
只有 payload 已持久化、`manifest.json` 以 no-replace 写入并同步后才删除标记。verify 要求根目录精确只有 `payload` 与 `manifest.json`,
|
||||
因此任何残留标记或额外文件都失败关闭。operator 必须保留现场或显式移走失败目录,再使用新路径重试。
|
||||
|
||||
清单不包含原始绝对路径、任意用户文件名或文件内容,只保存:Profile、时间、目录 plan digest、SQLite 两个 digest、源/暂存路径摘要,
|
||||
以及两个固定 payload group 的类别、计数、字节和语义 digest。语义 digest 绑定相对路径、entry kind、文件大小与内容摘要;源权限和时间
|
||||
由 plan digest 绑定,目标则强制归一化私有权限。
|
||||
|
||||
## 被拒绝的替代方案
|
||||
|
||||
### 直接复用 2.x tar 导出/导入
|
||||
|
||||
拒绝。它无法表达固定处置矩阵、双 digest 围栏、稳定 descriptor、Profile 预算和崩溃残留状态,也会把缓存和秘密材料混成一个恢复单元。
|
||||
|
||||
### 暂存成功后自动删除失败残留
|
||||
|
||||
拒绝。崩溃或 I/O 错误后不能证明每个创建对象仍属于本次调用;保留 `.incomplete` 比递归清理更容易审计,也避免错误删除 operator 资产。
|
||||
|
||||
### 不绑定 SQLite activation
|
||||
|
||||
拒绝。目录 payload 与主库 target 会成为两个可任意拼接的时间点,无法证明后续 config/Keyv 转换使用的是同一接管快照。
|
||||
|
||||
### 再拆一个 workspace package
|
||||
|
||||
拒绝。stage/verify 与 ADR-0477 inventory 是同一短生命周期 Local Owner capability,没有独立部署和依赖生命周期;继续内聚可避免
|
||||
“一个文件一个包”和平铺根源码两种碎片化。
|
||||
|
||||
## 影响
|
||||
|
||||
### 正面
|
||||
|
||||
- 完整 2.x 目录首次得到 no-replace、可重复 verify 的私有迁移输入;
|
||||
- 主库接管与目录接管通过真实 activation 写栅栏和摘要链绑定;
|
||||
- Edge/Standalone 复制器保持 64 KiB 固定缓冲并重复硬预算;
|
||||
- 崩溃残留不会被静默当作成功或被重试覆盖;
|
||||
- 能直接复制的资产与需要转换、外部保留、目标重建的资产保持物理隔离。
|
||||
|
||||
### 代价与限制
|
||||
|
||||
- stage 需要再次完整读取相关资产,并额外占用 payload 等量磁盘;
|
||||
- SQLite 写栅栏只保护主库;其他文件依靠逐文件稳定身份和 stage 前后完整计划复核,不是跨文件系统事务;
|
||||
- `.incomplete` 残留需要 operator 显式处置;
|
||||
- 本阶段只产出转换输入,不实现 config、Keyv、SSH 的目标模型转换;
|
||||
- 尚未把目录清单接入 systemd/OpenRC/Compose cutover lineage,也未完成固定物理 Edge 的断电、ENOSPC 和闪存写放大证明。
|
||||
|
||||
## 验证
|
||||
|
||||
- D-385 聚焦 data directory inspect/stage/verify `10/10`,使用真实 ADR-0476 SQLite 链;
|
||||
- 覆盖 reviewed payload、主库/缓存/日志排除、私有 mode、source/target drift、activation drift、no-replace crash residue、
|
||||
verify exact replay 与扩权命令;
|
||||
- Local Owner `200 total / 195 pass / 5 conditional skip / 0 fail`;backend
|
||||
`1,535 total / 1,533 pass / 2 conditional skip / 0 fail`,`pnpm build:back` 通过;
|
||||
- 18-package clean build/逐包测试、八项架构审计与按顺序执行的 14 档 artifact audit 全部通过;
|
||||
- GitNexus impact 最高 LOW,无跨模块 execution flow;change audit 作为提交前最后门禁。
|
||||
|
||||
本阶段不修改 PostgreSQL schema、ACL、repository、role、Pool、连接或 failover 语义,因此不重新占有 PostgreSQL HA 证明。
|
||||
|
||||
## 后续
|
||||
|
||||
- D-386:把 `config`、Keyv 与 `ssh.d` 转换输入变成版本化目标模型和恢复合同;
|
||||
- 在固定物理 Edge/NAS 上执行 stage/verify 的 RSS、I/O、磁盘峰值、ENOSPC 与受控断电演练;
|
||||
- 将目录 manifest digest 接入 systemd/OpenRC/Compose cutover、rollback 和发布证据 lineage。
|
||||
@@ -481,6 +481,7 @@
|
||||
| [ADR-0475](./ADR-0475-legacy-system-script-open-api-compatibility.md) | Legacy System、Script 与 Open API 兼容基线 | Accepted |
|
||||
| [ADR-0476](./ADR-0476-real-legacy-sqlite-upgrade-and-rollback-rehearsal.md) | 真实 Legacy SQLite 升级与回滚演练 | Accepted |
|
||||
| [ADR-0477](./ADR-0477-bounded-legacy-data-directory-inventory.md) | 有界 Legacy Data Directory 盘点 | Accepted |
|
||||
| [ADR-0478](./ADR-0478-private-legacy-data-directory-staging.md) | 私有 Legacy Data Directory 暂存与稳定校验 | Accepted |
|
||||
|
||||
## 规则
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# QingLong 2.x Data Directory 盘点
|
||||
# QingLong 2.x Data Directory 接管
|
||||
|
||||
本流程为完整 QingLong 2.x `data` 目录生成一个只读、确定性、按 Profile 有界的 3.0 接管计划。它不会复制、压缩、删除或修改
|
||||
任何文件,也不替代 [SQLite 接管流程](./ql3-local-sqlite-adoption.md)。
|
||||
本流程先为完整 QingLong 2.x `data` 目录生成一个只读、确定性、按 Profile 有界的 3.0 接管计划,再把审核过的资产 no-replace
|
||||
暂存并稳定校验。它不会删除或修改源文件,也不替代 [SQLite 接管流程](./ql3-local-sqlite-adoption.md)。
|
||||
|
||||
## 1. 前置条件
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
- `dataRoot` 必须由当前 UID 拥有,且 group/world 不可写;
|
||||
- command file 继续遵守 `ql3-adoption` 的当前 UID、canonical、单链接、`0600` 私有文件要求;
|
||||
- 生产盘点建议先停止 2.x writer。若文件或目录在盘点中变化,命令会失败关闭,不会给出部分成功计划。
|
||||
- stage 前必须已经完成 SQLite inspect、stage、verify 与 activation,并保留五个绝对路径和 `activationDigest`;
|
||||
- `deploymentRoot` 与 `stagingRoot` 的父目录必须是当前 UID 拥有的 canonical `0700` 目录;`stagingRoot` 必须尚不存在且位于
|
||||
`deploymentRoot` 内、`dataRoot` 外。
|
||||
|
||||
## 2. 执行 inspect
|
||||
|
||||
@@ -82,22 +85,96 @@ ql3-adoption run --command-file /secure/operator/ql3-data-directory-inspect.json
|
||||
超过任一限制都会以 `LOCAL_DATA_DIRECTORY_ADOPTION_CONFIGURATION_INVALID` 失败。不要为了通过门禁临时改名、删除或排除资产;先保留
|
||||
现场并决定它应拆分为外部恢复资产、在目标重新生成,还是进入后续人工迁移协议。
|
||||
|
||||
## 6. 常见失败
|
||||
## 6. 执行私有 stage
|
||||
|
||||
审核 `assessment=reviewable`、`primaryDatabaseFiles=1`、所有 `activeSqliteSidecars=0` 后,提交完整 plan 与 SQLite activation 双围栏:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "local-data-directory.adoption.stage",
|
||||
"options": {
|
||||
"deploymentRoot": "/opt/qinglong3/adoption",
|
||||
"dataRoot": "/opt/qinglong/data",
|
||||
"stagingRoot": "/opt/qinglong3/adoption/staging/reviewed-data",
|
||||
"profile": "edge",
|
||||
"expectedPlanDigest": "<64-hex-directory-plan-digest>",
|
||||
"sqlite": {
|
||||
"sourcePath": "/opt/qinglong/data/db/database.sqlite",
|
||||
"targetPath": "/opt/qinglong3/adoption/sqlite/qinglong3.sqlite",
|
||||
"recoveryPath": "/opt/qinglong3/adoption/sqlite/database.pre-ql3.sqlite",
|
||||
"manifestPath": "/opt/qinglong3/adoption/sqlite/adoption.json",
|
||||
"activationPath": "/opt/qinglong3/adoption/sqlite/activation.json",
|
||||
"expectedActivationDigest": "<64-hex-sqlite-activation-digest>"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
成功结果为 `status=staged`。暂存区固定包含 `payload/copy-reviewed/{scripts,upload}`、
|
||||
`payload/transform-input/{config,db,ssh.d}` 与私有 `manifest.json`;不存在的类别不会被制造。`db/database.sqlite` 及其 sidecar 不复制,
|
||||
主库恢复继续以 SQLite adoption 的 recovery/target/activation 为权威。日志、备份和缓存也不会进入 payload。
|
||||
|
||||
所有目录归一化为 `0700`,所有文件归一化为 `0600`。复制使用 64 KiB 缓冲,并再次执行当前 Profile 的条目、字节、单文件和深度预算。
|
||||
|
||||
## 7. 执行稳定 verify
|
||||
|
||||
保存 stage 返回的 `manifestDigest`,使用同一组路径和 SQLite activation 执行:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "local-data-directory.adoption.verify",
|
||||
"options": {
|
||||
"deploymentRoot": "/opt/qinglong3/adoption",
|
||||
"dataRoot": "/opt/qinglong/data",
|
||||
"stagingRoot": "/opt/qinglong3/adoption/staging/reviewed-data",
|
||||
"profile": "edge",
|
||||
"expectedManifestDigest": "<64-hex-directory-manifest-digest>",
|
||||
"sqlite": {
|
||||
"sourcePath": "/opt/qinglong/data/db/database.sqlite",
|
||||
"targetPath": "/opt/qinglong3/adoption/sqlite/qinglong3.sqlite",
|
||||
"recoveryPath": "/opt/qinglong3/adoption/sqlite/database.pre-ql3.sqlite",
|
||||
"manifestPath": "/opt/qinglong3/adoption/sqlite/adoption.json",
|
||||
"activationPath": "/opt/qinglong3/adoption/sqlite/activation.json",
|
||||
"expectedActivationDigest": "<64-hex-sqlite-activation-digest>"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
verify 会重新验证目录计划、SQLite activation/source/target、清单 exact shape、私有权限和完整 payload 语义摘要。成功结果为
|
||||
`status=verified`,且 evidence 应与 stage 的低敏 evidence 一致。
|
||||
|
||||
## 8. 崩溃残留
|
||||
|
||||
stage 创建暂存根后立即写入 `.incomplete`。只有 payload 和 `manifest.json` 都持久化后才删除它。命令失败或进程崩溃后:
|
||||
|
||||
- 不要直接把残留目录当作恢复资产;
|
||||
- 不要在原路径重试,stage 会 no-replace 拒绝;
|
||||
- 先保存现场用于诊断,再由 operator 显式移走残留目录,并使用一个新的空路径重试;
|
||||
- verify 遇到 `.incomplete`、额外文件或缺失文件一律失败关闭。
|
||||
|
||||
## 9. 常见失败
|
||||
|
||||
- 根目录或条目 group/world 可写:修正 ownership/permission 后重新盘点;
|
||||
- symlink、硬链接或特殊文件:保留现场,确认来源和目标后人工处置;盘点不会跟随或读取;
|
||||
- 目录在盘点中变化:停止 2.x writer、同步器、下载器和仓库更新后重试;
|
||||
- 单文件或总内容超过 Profile 预算:不要改用 `tar` 绕过;为该资产设计独立流式迁移/外部保留流程;
|
||||
- 未知顶层条目:根据插件或用户资产来源登记明确处置,再进入后续 staging 设计。
|
||||
- activation 不匹配:重新执行 SQLite verify/activation,不能只替换 digest;
|
||||
- `stagingRoot` 已存在:检查是否为崩溃残留,禁止覆盖或合并;
|
||||
- stage/verify 后源或目标 drift:停止所有 writer,回到 inspect,生成并重新审核新的 plan。
|
||||
|
||||
## 7. 当前边界
|
||||
## 10. 当前边界
|
||||
|
||||
本命令只生成只读计划。它尚不:
|
||||
本流程已经提供 inspect、私有 stage 和稳定 verify。它仍不:
|
||||
|
||||
- 复制、压缩、删除或转换任何资产;
|
||||
- 创建 no-replace stage、verify manifest 或 recovery;
|
||||
- 把目录计划绑定到 SQLite `planDigest`、`manifestDigest` 或 `activationDigest`;
|
||||
- 删除或修改任何源资产;
|
||||
- 把 `config`、Keyv 或 `ssh.d` 自动转换为 3.0 目标模型;
|
||||
- 把历史日志/备份复制到默认目标,或复用跨架构 repo/dependency cache;
|
||||
- 授权 service-manager/Compose cutover 或 Legacy rollback;
|
||||
- 证明固定物理路由器/NAS 上的耗时、RSS、I/O、磁盘峰值和断电恢复。
|
||||
|
||||
在后续 D-385 stage/verify 合同完成前,保留 inspect 输出和原始 2.x data directory,不要把目录计划当作自动迁移完成证明。
|
||||
只有 `status=verified` 仍不是 cutover 授权。继续保留原始 2.x data directory、SQLite recovery 和两份 manifest,等待后续转换与部署
|
||||
lineage 完成。
|
||||
|
||||
Reference in New Issue
Block a user