- 迁移:调用结算与配置验证(V0056)、来源当前授权(V0057)、成品补充验证回执(V0058)、检索索引代次(V0059)、 评测单元复用来源(V0060);新表按既有约定加只追加守卫与角色授权。 - 流程模板与载入口径同步(内联保护字段改为登记类型约束);旧库迁移工具链按新表结构对齐。 - 配置:提供方模板与运行配置同步角色策略版本;角色策略白名单新增 qwen3.8-flash(见收尾报告待裁决项: 该模型精确身份与独立性尚未核验,且策略版本号未随白名单升版)。
294 lines
14 KiB
Python
294 lines
14 KiB
Python
"""旧源只读快照、纯分流与隔离目标候选导入;不调用模型、确认或切换生产。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import hashlib
|
||
import os
|
||
import sys
|
||
import tempfile
|
||
from collections import Counter
|
||
from pathlib import Path
|
||
|
||
from 建立映射 import 稳定JSON, 读取JSON, 旧快照, 映射配置, 检查目标快照, 迁移错误
|
||
from 知识记录分流 import 分流批次
|
||
|
||
文件上限 = 16 * 1024 * 1024
|
||
|
||
|
||
def _读文件(路径: Path):
|
||
with 路径.open("rb") as 文件:
|
||
内容 = 文件.read(文件上限 + 1)
|
||
if len(内容) > 文件上限:
|
||
raise 迁移错误("input_too_large", "快照或映射超过16MiB,请显式分批,不截断输入")
|
||
return 读取JSON(内容.decode("utf-8"))
|
||
|
||
|
||
def _保存文件(路径: Path, 文本: str):
|
||
临时 = None
|
||
try:
|
||
with tempfile.NamedTemporaryFile(
|
||
mode="w", encoding="utf-8", dir=路径.parent, prefix=".迁移分流-", delete=False
|
||
) as 文件:
|
||
临时 = Path(文件.name)
|
||
文件.write(文本)
|
||
文件.flush()
|
||
os.fsync(文件.fileno())
|
||
try:
|
||
# 同目录硬链接保证完整可见且不覆盖已有文件;临时文件默认仅当前用户可读写。
|
||
os.link(临时, 路径)
|
||
except FileExistsError:
|
||
if 路径.read_text(encoding="utf-8") != 文本:
|
||
raise 迁移错误("output_conflict", "输出已存在且内容不同,请另选路径") from None
|
||
finally:
|
||
if 临时 is not None:
|
||
临时.unlink(missing_ok=True)
|
||
|
||
|
||
def _导出源(值) -> int:
|
||
源 = 值.连接说明 if 值.command == "读取PG" else 值.副本
|
||
输入路径 = {源.resolve()}
|
||
if 值.主键范围:
|
||
输入路径.add(值.主键范围.resolve())
|
||
if 值.输出.resolve() in 输入路径:
|
||
raise 迁移错误("output_is_input", "输出不能覆盖连接说明、旧库副本或范围文件")
|
||
范围 = _读文件(值.主键范围) if 值.主键范围 else {}
|
||
参数 = {
|
||
"数据集": 值.数据集,
|
||
"表": tuple(值.表),
|
||
"快照版本": 值.快照版本,
|
||
"最大行数": 值.最大行数,
|
||
"范围": 范围,
|
||
}
|
||
if 值.command == "读取PG":
|
||
from 读取旧PG import 读取旧库 as 读取PG
|
||
|
||
结果 = 读取PG(源, schema=值.schema, 租户=值.租户, **参数)
|
||
else:
|
||
from 读取旧SQLite import 读取旧库 as 读取SQLite
|
||
|
||
结果 = 读取SQLite(源, **参数)
|
||
_保存文件(值.输出, 稳定JSON([r.导出() for r in 结果]) + "\n")
|
||
print(f"只读快照:{len(结果)}条(未导入目标库)")
|
||
return 0
|
||
|
||
|
||
def _文件命令(值) -> int:
|
||
from 读取旧文件 import 核对文件保全, 读取文件集
|
||
|
||
try:
|
||
if 值.command == "读取文件":
|
||
结果 = 读取文件集(值.源根, _读文件(值.清单), 数据集=值.数据集, 输出目录=值.输出目录)
|
||
print(f"文件保全:{len(结果)}项(未写业务候选、未确认)")
|
||
return 0
|
||
if 值.输出.resolve().is_relative_to(值.文件回执.parent.resolve()):
|
||
raise 迁移错误("output_is_input", "文件核对报告不得写入被核对保全根")
|
||
结果 = {
|
||
**核对文件保全(值.文件回执),
|
||
"stage": "file_preservation_only",
|
||
"production_switch_authorized": False,
|
||
"package_acceptance_claimed": False,
|
||
}
|
||
_保存文件(值.输出, 稳定JSON(结果) + "\n")
|
||
print(
|
||
"文件保全核对:" + ("通过" if 结果["passed"] else "有缺口") + ";不代表PG对象或整包通过"
|
||
)
|
||
return 0 if 结果["passed"] else 2
|
||
except (迁移错误, OSError, ValueError, TypeError, KeyError) as 错:
|
||
代码 = 错.code if isinstance(错, 迁移错误) else "file_operation_failed"
|
||
raise 迁移错误(代码, "文件命令未完成;不回显原文、编码内容或凭据") from None
|
||
|
||
|
||
def _检查实例命令(值) -> int:
|
||
from muse.共享.错误 import Muse错误
|
||
from muse.配置 import 数据库引用
|
||
from 导入新库 import 检查实例
|
||
|
||
try:
|
||
if 值.输出.resolve() in {值.端点记录.resolve(), 值.管理引用.resolve()}:
|
||
raise 迁移错误("output_is_input", "实例报告不能覆盖端点记录或凭据引用")
|
||
结果 = 检查实例(
|
||
端点记录=值.端点记录,
|
||
管理引用=数据库引用("受控存储", str(值.管理引用)),
|
||
)
|
||
_保存文件(值.输出, 稳定JSON(结果) + "\n")
|
||
except (迁移错误, Muse错误, OSError, ValueError, TypeError, KeyError) as 错:
|
||
代码 = 错.code if isinstance(错, 迁移错误) else "target_inspect_failed"
|
||
raise 迁移错误(代码, "只读实例检查未完成;不回显连接串、原始错误或凭据") from None
|
||
print("只读实例与数据库身份已保存;未授权迁移、未创建或修改数据库")
|
||
return 0
|
||
|
||
|
||
def _初始化目标命令(值) -> int:
|
||
from muse.配置 import 数据库引用
|
||
from 导入新库 import 初始化目标
|
||
|
||
回执 = 初始化目标(
|
||
允许实例=值.允许实例,
|
||
管理引用=数据库引用("受控存储", str(值.管理引用)),
|
||
维护模板=数据库引用("受控存储", str(值.维护模板)),
|
||
应用模板=数据库引用("受控存储", str(值.应用模板)),
|
||
输出目录=值.输出目录,
|
||
)
|
||
print("已创建隔离目标:" + 回执["database"] + "(未导入业务候选)")
|
||
return 0
|
||
|
||
|
||
def _目标命令(值) -> int:
|
||
from muse.启动 import 构建
|
||
from muse.接入.创作操作 import 创作身份
|
||
from muse.配置 import 数据库引用, 读取配置
|
||
from 切换检查 import 检查切换条件
|
||
from 导入新库 import 导入快照
|
||
from 核对结果 import 核对迁移结果
|
||
|
||
输入路径 = {
|
||
p.resolve() for p in (值.快照, 值.映射, 值.配置, 值.允许实例, 值.管理引用, 值.目标回执)
|
||
}
|
||
if 值.文件回执:
|
||
输入路径.add(值.文件回执.resolve())
|
||
if 值.输出.resolve().is_relative_to(值.文件回执.parent.resolve()):
|
||
raise 迁移错误("output_is_input", "目标报告不得写入文件保全根")
|
||
if getattr(值, "当前快照", None):
|
||
输入路径.add(值.当前快照.resolve())
|
||
if getattr(值, "来源回放", None):
|
||
输入路径.add(值.来源回放.resolve())
|
||
if 值.输出.resolve() in 输入路径:
|
||
raise 迁移错误("output_is_input", "报告不能覆盖任何输入、配置或回执")
|
||
输入 = _读文件(值.快照)
|
||
if not isinstance(输入, list) or not all(isinstance(r, dict) for r in 输入):
|
||
raise 迁移错误("snapshot_invalid", "快照必须是记录数组")
|
||
快照 = [旧快照.从载荷(r) for r in 输入]
|
||
检查目标快照(快照)
|
||
当前 = None
|
||
if 值.command == "切换检查":
|
||
当前输入 = _读文件(值.当前快照)
|
||
if not isinstance(当前输入, list) or not all(isinstance(r, dict) for r in 当前输入):
|
||
raise 迁移错误("snapshot_invalid", "当前快照必须是记录数组")
|
||
当前 = [旧快照.从载荷(r) for r in 当前输入]
|
||
检查目标快照(当前, 范围名称="当前")
|
||
映射 = 映射配置(_读文件(值.映射))
|
||
配置 = 读取配置(值.配置)
|
||
if 配置.HTTP is None:
|
||
raise 迁移错误("target_denied", "目标命令需要已有本地作者身份配置,不采用映射作者作为认证")
|
||
装配, 身份 = 构建(配置), 创作身份(配置.HTTP.作者ID, 配置)
|
||
参数 = {
|
||
"允许实例": 值.允许实例,
|
||
"管理引用": 数据库引用("受控存储", str(值.管理引用)),
|
||
"目标回执": 值.目标回执,
|
||
"文件回执": 值.文件回执,
|
||
}
|
||
# 常驻数据库工厂只在生命周期内开池;目标命令借的还是同一个受控目标。
|
||
with 装配.生命周期():
|
||
if 值.command == "导入目标":
|
||
结果 = 导入快照(装配, 身份, 快照, 映射, **参数)
|
||
通过 = 结果["all_accounted"] and not 结果["states"].get("quarantined")
|
||
elif 值.command == "核对目标":
|
||
请求 = _读文件(值.来源回放) if 值.来源回放 else None
|
||
结果 = 核对迁移结果(装配, 身份, 快照, 映射, 来源回放=请求, **参数)
|
||
通过 = 结果["data_checks_passed"]
|
||
else:
|
||
assert 当前 is not None
|
||
结果 = 检查切换条件(装配, 身份, 快照, 当前, 映射, **参数)
|
||
通过 = 结果["data_checks_passed"]
|
||
_保存文件(值.输出, 稳定JSON(结果) + "\n")
|
||
print(
|
||
值.command
|
||
+ (":本范围数据检查通过" if 通过 else ":有隔离或未闭合项")
|
||
+ ";未确认候选、未授权生产切换"
|
||
)
|
||
return 0 if 通过 else 2
|
||
|
||
|
||
def main(argv: list[str] | None = None) -> int:
|
||
参数 = argparse.ArgumentParser(description=__doc__)
|
||
子命令 = 参数.add_subparsers(dest="command", required=True)
|
||
分流 = 子命令.add_parser("分流", help="只读快照与映射,写出分类计划,不导入数据库")
|
||
分流.add_argument("--快照", type=Path, required=True)
|
||
分流.add_argument("--映射", type=Path, required=True)
|
||
分流.add_argument("--输出", type=Path, required=True)
|
||
for 名, 源参数 in (("读取PG", "--连接说明"), ("读取SQLite", "--副本")):
|
||
读取 = 子命令.add_parser(名, help="只读明确旧源,输出完整原快照,不导入或确认")
|
||
读取.add_argument(源参数, type=Path, required=True)
|
||
读取.add_argument("--数据集", required=True)
|
||
读取.add_argument("--表", action="append", required=True)
|
||
读取.add_argument("--快照版本", required=True)
|
||
读取.add_argument("--最大行数", type=int, default=10000)
|
||
读取.add_argument("--主键范围", type=Path)
|
||
读取.add_argument("--输出", type=Path, required=True)
|
||
if 名 == "读取PG":
|
||
读取.add_argument("--schema", required=True)
|
||
作用域 = 读取.add_mutually_exclusive_group(required=True)
|
||
作用域.add_argument("--租户", type=int)
|
||
作用域.add_argument("--全局", action="store_true")
|
||
文件 = 子命令.add_parser("读取文件", help="按明确清单只读源根,生成私有原字节保全与快照")
|
||
for 名 in ("源根", "清单", "输出目录"):
|
||
文件.add_argument("--" + 名, type=Path, required=True)
|
||
文件.add_argument("--数据集", required=True)
|
||
核对文件 = 子命令.add_parser("核对文件", help="只读核对实际副本和文件回执,不宣称PG或整包通过")
|
||
for 名 in ("文件回执", "输出"):
|
||
核对文件.add_argument("--" + 名, type=Path, required=True)
|
||
实例 = 子命令.add_parser("检查实例", help="只读核对明确端点的实例与数据库目录身份")
|
||
for 名 in ("端点记录", "管理引用", "输出"):
|
||
实例.add_argument("--" + 名, type=Path, required=True)
|
||
初始化 = 子命令.add_parser("初始化目标", help="只在获准实例新建随机目标库,不接受既有库")
|
||
for 名 in ("允许实例", "管理引用", "维护模板", "应用模板", "输出目录"):
|
||
初始化.add_argument("--" + 名, type=Path, required=True)
|
||
for 名 in ("导入目标", "核对目标", "切换检查"):
|
||
目标 = 子命令.add_parser(名, help="隔离目标候选写入或只读对账,不确认和切换")
|
||
for 字段 in ("快照", "映射", "配置", "允许实例", "管理引用", "目标回执", "输出"):
|
||
目标.add_argument("--" + 字段, type=Path, required=True)
|
||
目标.add_argument("--文件回执", type=Path)
|
||
if 名 == "核对目标":
|
||
目标.add_argument("--来源回放", type=Path)
|
||
if 名 == "切换检查":
|
||
目标.add_argument("--当前快照", type=Path, required=True)
|
||
值 = 参数.parse_args(argv)
|
||
try:
|
||
if 值.command == "检查实例":
|
||
return _检查实例命令(值)
|
||
if 值.command in {"读取文件", "核对文件"}:
|
||
return _文件命令(值)
|
||
if 值.command in {"初始化目标", "导入目标", "核对目标", "切换检查"}:
|
||
from muse.共享.错误 import Muse错误
|
||
|
||
try:
|
||
return _初始化目标命令(值) if 值.command == "初始化目标" else _目标命令(值)
|
||
except Muse错误 as 错:
|
||
raise 迁移错误(
|
||
错.错误码, "目标命令未完成;未确认或切换,错误细节不回显正文或凭据"
|
||
) from None
|
||
if 值.command != "分流":
|
||
return _导出源(值)
|
||
if 值.输出.resolve() in {值.快照.resolve(), 值.映射.resolve()}:
|
||
raise 迁移错误("output_is_input", "输出不能覆盖快照或映射")
|
||
输入, 配置 = _读文件(值.快照), _读文件(值.映射)
|
||
if not isinstance(输入, list) or not all(isinstance(r, dict) for r in 输入):
|
||
raise 迁移错误("snapshot_invalid", "快照文件必须是记录对象数组")
|
||
if not isinstance(配置, dict):
|
||
raise 迁移错误("mapping_invalid", "映射文件必须是对象")
|
||
快照 = [旧快照.从载荷(r) for r in 输入]
|
||
映射 = 映射配置(配置)
|
||
结果 = 分流批次(快照, 映射)
|
||
统计 = dict(Counter(r.disposition for r in 结果))
|
||
计划 = {
|
||
"schema_version": 1,
|
||
"stage": "classification_only",
|
||
"snapshot_hash": hashlib.sha256(稳定JSON(输入).encode()).hexdigest(),
|
||
"mapping_hash": hashlib.sha256(稳定JSON(映射.值).encode()).hexdigest(),
|
||
"source_count": len(输入),
|
||
"distinct_source_count": len({r.source_key for r in 结果}),
|
||
"dispositions": 统计,
|
||
"results": [r.导出() for r in 结果],
|
||
}
|
||
_保存文件(值.输出, 稳定JSON(计划) + "\n")
|
||
print("分流计划(未导入数据库):" + 稳定JSON(统计))
|
||
return 2 if 统计.get("隔离", 0) else 0
|
||
except (迁移错误, OSError, ValueError) as 错:
|
||
print(str(错), file=sys.stderr)
|
||
return 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|