轻量级 CLI 契约
CLI 是库的可选使用入口,使用 cli extra 中的 Typer 与 Rich。命令仅完成参数转换、公共 API 调用、输出和进程生命周期管理。当前已实现显式 JSON 文件/快照输入、离线验证和 BMP 分析、VRP/ASPA 查询、来源比较与导出,以及 TOML 配置检查、RTR/文件/HTTP 来源的在线同步、状态和事件观察、SQLite/DuckDB 拥有者持久化及 SQLite 只读共享。还提供 RTR 录制回放、HTTP 远程调用与单一拥有者服务命令。当前可用参数以从命令定义生成的参考和 --help 为准。
可执行的离线命令示例见 examples/cli.py。运行 python examples/cli.py 使用临时合成 JSON 和显式评价时间,不访问公网。全局选项放在子命令之前,例如 rpkiparrot --json-file source.json --max-age 3600 --at 2026-01-01T00:00:00Z --format json validate origin 192.0.2.0/24 64496。
在线文件来源及配置检查示例见 examples/cli_online.py。安装 rpkiparrot[cli] 后运行 python examples/cli_online.py,它创建临时 TOML 和完整 JSON,在真实子进程中执行配置检查、同步、验证与就绪状态查询;每条命令拥有并清理自己的 Client。
来源选择
每次命令只选择一种输入运行模式,不隐式寻找正在运行的另一个进程:
| 选项 | 行为 |
|---|---|
| --config PATH | 显式加载组和来源配置,运行本命令拥有的客户端 |
| --json-file PATH | 单份生产者 JSON;需 --json-format 和可确定的 --max-age/--valid-until 策略 |
| --snapshot PATH | 本库完整导出;期限从原文件保留 |
| --rtr HOST:PORT | 单个直接来源;--tls 启用 TLS,--ssh 启用 SSH,两者互斥;IPv6 地址使用方括号 |
| --database PATH | 只读 SQLite 当前快照,使用共享读取契约,不创建 owner |
| --remote URL | HTTP 服务,需 http extra,不隐式启动服务 |
--json-format 默认 auto,不能识别时要求用户指定。--at UTC_TIME 显式选择离线 JSON/快照/回放评价,不允许给在线 RTR 或远程查询任意替换服务器时间。没有来源时报告参数错误,不从当前目录自动挑选一个 JSON 或数据库。
配置文件中已声明 owner 持久化时按配置执行。单来源 sync 可用 --backend sqlite|duckdb 和 --db-path PATH 指定写入目标;二者必须同时出现,不与只读 --database 混用。DuckDB 活动共享由拥有者服务提供;--database 只接受 SQLite,不回退为 DuckDB 拥有者。单来源的这两个选项位于 sync 之后,不覆盖显式 TOML 的 persistence 段。
全局 --format text|json|jsonl、--timeout(remote 模式默认 180 秒,其他模式默认 30 秒)、--quiet、--verbose、--no-color、--help、--version。持续流支持 jsonl,有限单条响应支持 json;不支持的组合执行前拒绝。
普通在线命令的就绪等待独立于 HTTP 来源请求和持久化 flush 预算。百万级输入可显式使用较大的等待,例如 rpkiparrot --config sources.toml --timeout 1800 sync;来源 request_timeout 或 flush_timeout 的较大默认值不会自动扩大 CLI 的就绪等待。
显式配置加载
--config 使用 Python 3.11+ 标准库 tomllib,只接受 schema_version = 1,文件大小上限 1 MiB。未知键、缺失必填字段、重复 ID、非法模式或非有限时间均报配置错误;错误不回显 TOML 内容。所有文件路径相对 TOML 所在目录解析,加载器不扫描插件、自动读取其他配置或隐式加载 Python 代码。
来源 kind 固定为 rtr、json_file、http_json。字段与公共配置对象一致;JSON 来源的 freshness 为嵌套表,valid_until 接受带时区 TOML 日期时间或 RFC 3339 字符串。运行中自定义 JsonReader 通过 Python API 显式注入,CLI 不从配置自动导入适配器。
schema_version = 1
required = ["vrp"]
[[groups]]
id = "primary"
priority = 0
[[groups.sources]]
id = "local"
kind = "json_file"
path = "./source.json"
format = "routinator-json"
poll_interval = 30
[groups.sources.freshness]
max_age = 3600
[cli]
format = "json"
timeout = 30
no_color = true
log_level = "WARNING"
[cli] 只接受上述四个字段。显式命令行值优先于对应的 RPKIPARROT_CONFIG、RPKIPARROT_FORMAT、RPKIPARROT_TIMEOUT、RPKIPARROT_NO_COLOR、RPKIPARROT_LOG_LEVEL,然后使用文件值和默认值;未指定的命令行默认值不会覆盖 TOML。--no-color/--color 可显式覆盖文件或对应环境变量,仍尊重 NO_COLOR。日志级别为 DEBUG、INFO、WARNING、ERROR 或 CRITICAL;quiet/verbose 只调整日志,不吞掉结果或错误。命令结束恢复宿主的库日志设置,不修改根日志处理器。
HTTP 来源可在 headers 表提供固定请求头,通过 headers_env 表把指定头名映射到明确的环境变量名,例如 Authorization = "MY_RPKI_AUTHORIZATION"。后者变量的完整值作为头值,缺失时失败,同一头不能同时指定两种来源。配置检查只输出来源/组 ID 和所需能力,不打印 URL、头值、证书内容或私钥。来源凭据只读取明确配置的环境变量绑定,SSH 绑定的读取时机见下文。
rpkiparrot --config client.toml config check 构造公共配置并进入 Client 上下文,验证格式注册、可选依赖和 TLS/SSH 文件,并读取已绑定的 SSH 凭据,但不运行来源任务、不连接缓存、不下载或解析来源载荷;成功不代表数据已同步或远端身份已经核实。配置包含持久化时打开并验证后端、恢复已保存状态,空数据库可初始化 schema;检查不运行写入器,不改写已有持久化 head,也不代表来源内容已通过校验。SQLite 拥有者需要 sqlite extra;DuckDB 拥有者需要 duckdb extra。
直接 RTR 模式接受 --rtr HOST[:PORT],IPv6 用方括号;缺省 TCP 323/TLS 324。--tls 配合 --trust-profile-id 及 --client-cert-file(可选 --client-key-file、--ca-file);IP 端点必须额外给 DNS --server-name。--local-address 指定源 IP,--min-version/--max-version 指定协议范围,实验性 v2 须显式选择;--v2-profile 固定为 8210bis-27(默认)、8210bis-10 或 8210bis-13,分别采用对应草案的线格式,不能从收到的字节自动推断。上述选项只用于直接 RTR;TOML 来源使用自己的字段。--at 不允许给在线配置/RTR/共享数据库替换时间,单份 JSON 和导出文件仍保留显式离线评价入口。
SSH 模式安装 rpkiparrot[cli,ssh],使用 CLI 的 asyncio 入口连接固定的 rpki-rtr subsystem,缺省端口 22。--ssh 必须同时指定 --ssh-user、--ssh-known-hosts、--trust-profile-id,并通过至少一个 --ssh-key 或 --ssh-password-env 提供认证。--ssh-key 可重复;加密私钥通过 --ssh-passphrase-env 指定环境变量名。CLI 不接受明文密码/口令参数,不读取用户 SSH 配置、默认私钥或 SSH agent,不提供交互密码提示。主机密钥须预先经可信渠道核实并写入 known-hosts 文件;未知、变更及吊销的主机密钥均拒绝,不自动接受首次连接。
rpkiparrot --rtr cache.example --ssh --ssh-user rpki \
--ssh-known-hosts ./known_hosts --ssh-key ./rtr_client_key \
--trust-profile-id cache-a-ssh-v1 --format json config check
rpkiparrot --rtr cache.example:2222 --ssh --ssh-user rpki \
--ssh-known-hosts ./known_hosts --ssh-password-env RPKI_SSH_PASSWORD \
--trust-profile-id cache-a-ssh-v1 --format json sync
TOML 仍使用组内来源;SSH 参数放在 [groups.sources.ssh],路径相对配置文件目录解析,client_keys 必须是路径数组。只允许用户名、known-hosts、私钥路径和显式环境变量名,不接受 password、passphrase 或 Python provider。以下示例中的文件和服务器由调用方提供:
schema_version = 1
[[groups]]
id = "primary"
priority = 0
[[groups.sources]]
id = "cache-a"
kind = "rtr"
host = "cache.example"
transport = "ssh"
trust_profile_id = "cache-a-ssh-v1"
[groups.sources.ssh]
username = "rpki"
known_hosts = "./known_hosts"
client_keys = ["./rtr_client_key"]
# passphrase_env = "RPKI_SSH_KEY_PASSPHRASE"
# password_env = "RPKI_SSH_PASSWORD"
password_env/passphrase_env 仅绑定变量名;普通 TOML 加载、帮助和补全不读取秘密。进入 Client 上下文准备 SSH 来源时读取一次,缺失或空值使准备失败,错误不回显凭据;重连复用已准备的材料。修改凭据或主机信任需重新配置并变更 trust_profile_id,运行中的来源不会监视文件或环境变量。--ssh-* 只用于直接 --rtr --ssh,不能覆盖 TOML、JSON、数据库或远程 HTTP 模式的配置。
有限在线验证、查询和导出先用公共 Client 等待所需能力,取得一个独立快照,再关闭 Client;后续公共查询仍逐次检查该快照的原期限。sync 默认等待配置的 required,重复的 --required 显式覆盖本次等待;--json-file 的 sync/watch 使用完整在线文件来源,普通文件查询保留单次读取行为。
status 默认立即采样本命令拥有的 Client,输出 observation=command_owned_client,不暗示正在观察另一个进程。显式 --database 使用 observation=sqlite_shared_reader,输出读取者自己的 snapshot_id,以及所观察写入者的 upstream_id。显式 --required 才等待就绪;超时仍输出 StatusReport 和 ready_timeout=true,退出码为 3。没有显式等待的状态可能仍为未加载。
watch 和 sync --watch 持续流只接受 text/jsonl。watch 的 initial.data 包含完整 rpkiparrot-snapshot 交换对象,initial.initial_status 为同一注册点的当前 StatusReport;后续行是 snapshot_event/status_event;丢失事件时输出 resync_required,再取得新 initial,不伪造连续事件。初始序列化和持续 stdout 写入在受管理线程中执行,慢输出不会阻塞协议任务;事件积压仍受订阅上限约束。中断和断管不输出成功 summary。--timeout 限制初始能力等待,不是持续订阅的自动截止时间。
持久化命令
rpkiparrot --json-file source.json --max-age 3600 --format json sync --backend sqlite --db-path cache.sqlite
rpkiparrot --database cache.sqlite --format json status
rpkiparrot --database cache.sqlite --format json validate origin 192.0.2.0/24 64496
rpkiparrot --database cache.sqlite --format jsonl watch
单来源同步或 TOML 中启用持久化的同步在公共 flush() 成功后才输出 persist_receipt,其 snapshot_id、提交时间、schema_version 和摘要来自真实后端回执。写入失败不输出成功同步行。写入可合并更晚的完整快照,回执须覆盖本次等待的目标;它不承诺每一个瞬间版本都逐一落盘。
--database 使用 SharedSnapshotClient 的只读 SQLite 模式,不启动来源、不取得 owner 锁、不创建文件或 schema。它只需 cli extra,读连接使用标准库 sqlite3。sync 在此模式仅等待共享快照就绪,返回 persist_receipt=null,不声称自己完成了写入。config check 打开并验证该只读数据库;路径不存在、schema 不兼容或损坏均明确失败。普通查询、验证、来源比较与导出从同一个读者快照取值;writer 退出后仍遵守原期限。watch 随公开共享契约处理轮询跳代、writer 重启与独立过期事件。
可执行例子 examples/cli_persistence.py 通过真实子进程演示 SQLite 同步、配置检查和只读验证;安装 rpkiparrot[cli,sqlite] 后运行。它保留并核对写入者与读取者的不同快照身份。
远程调用
--remote URL 使用 RemoteClient 调用明确的 HTTP 服务;安装 rpkiparrot[cli,http],不要求本机安装服务框架或数据库驱动。--remote-token-env NAME 只读取该名字的环境变量作为 bearer,--remote-ca-file PATH 提供显式 CA 文件;两者只适用于 remote 模式。CLI 不接受明文 token 参数,不跟随重定向或读取环境代理;非回环地址传递 bearer 必须使用 HTTPS。--at 不适用于在线远程结果。
未指定 CA 文件时使用 HTTPX 的 certifi 信任集合,不读取证书环境变量或追加操作系统证书库。显式 CA 的文件读取与传输创建在受管理线程中完成;两种路径都由本库传输管理连接、TLS 握手与取消清理。
rpkiparrot --remote https://rpki.example --remote-token-env MY_RPKI_TOKEN --format json status
rpkiparrot --remote https://rpki.example --remote-token-env MY_RPKI_TOKEN --format json validate origin 192.0.2.0/24 64496
rpkiparrot --remote https://rpki.example --remote-token-env MY_RPKI_TOKEN --format jsonl watch
有限验证、查询、来源比较和导出固定一个远程 SnapshotRef;分页途中到期或淘汰明确失败,不重新固定快照拼接结果。领域计算由服务执行;初始 watch 展示才请求该固定引用的完整导出。status 标明 observation=remote_service;sync 只等待远端就绪,persist_receipt=null,不声称本命令写入远程数据库。config check 检查远程连接及服务状态,不修改服务器配置。服务端热配置仍由宿主 Python API 负责。
服务与远程 CLI 示例的 --cli 模式会启动临时本机服务,并在实际子进程中调用远程 CLI,运行完成后释放监听器。
命令与公共能力
| 命令 | 关键参数 | 调用与完成条件 |
|---|---|---|
| config check | --config 或直接来源选项 | 配置和启动依赖准备检查,不运行来源网络任务 |
| sync | --required vrp/aspa,可重复;--watch | wait_ready;默认一次,持久化启用时等待 flush,--watch 才持续运行 |
| status | 来源选择、--required | get_status;标明当前观察对象和 sampled_at |
| validate origin | PREFIX、ASN 或 --input PATH/-;--explain | validate_origin / validate_origins;单条 ASN 支持 none |
| validate aspa | --path、--local-asn、--neighbor-asn、--relationship 或 --input;--explain | 标准 ASPA;路径字符串仅支持空格分隔普通 AS_SEQUENCE,复杂段用 JSON |
| analyze bmp | --input PATH/-;--explain | analyze_bmp_path;独立 assessment 输出 |
| query vrps | --covering、--asn、--source-id、--family | covering_vrps / iter_vrps;不能混合不同快照 |
| query aspa | CUSTOMER,或 --all [--source-id ID],二者互斥 | aspa_providers / iter_aspas;遍历固定快照,保留 present 与 providers 区别 |
| sources diff | LEFT RIGHT | compare_sources,不决定哪方可信 |
| export | --output PATH/- | export_snapshot,完整交换格式 |
| watch | --required 可选 | 原子 initial 和后续事件,遇 ResyncRequired 输出标志并重新开始观察 |
| record | --output PATH、--duration 秒 | 显式 Recorder 包装 RTR,默认 duration=60 秒;--watch 才无限期至中断 |
| replay | PATH、--at UTC_TIME、--allow-incomplete | 离线 replay,完整性和差异写入报告 |
| serve | --config PATH、--host、--port | 可选 service 应用入口,单一 owner |
ROV 批量输入 JSONL 每行含 input_id、prefix、asn;ASPA 每行含 input_id、path、context,可选顶层 afi;BMP 使用独立 BMP context,并可带同一顶层 afi。忽略空白行但保留逻辑输入顺序;语法错误保留物理行号。--input - 读取 stdin;完整有限批次先验证大小再调用库的批量 API,不按行临时取得不同快照。
ASPA 查询、验证和 BMP 分析接受 --afi ipv4|ipv6(整数编号 1|2 也可),对应公共 Afi.IPV4/IPV6,不是网络版本号 4/6。省略表示双族共同视图;两族授权或可用性不同会明确报错,不能自动挑选一族。JSONL 的顶层 afi 可为整数 1/2、上述族名或 null;缺少该字段使用命令的 --afi,显式字段与 --afi 冲突则该行返回输入错误。每个有限批次固定一个快照,同族批次等待指定族,混合族或共同视图批次等待双族可用。
批次超过 max_batch_items 时失败,用户显式拆批并接受不同快照语义。JSONL 展示可逐条写出已经完成的有限批次,最终写 summary 表明 complete 和计数;管道中断不能宣称接收者得到完整结果。
边界解析已经失败的行保留为对应 BatchItem.error;能构造为公共输入模型的行一起交给同一快照上的库批量接口,再按原索引组合结果。CLI 不自行计算 ROV/ASPA 状态,也不因一个坏行重新取得快照。读取文件整体失败或超过上限则整次失败。
输出和退出码
stdout 只写结果;日志、进度和操作错误写 stderr。有限 JSON 使用统一结果 envelope;JSONL 每行是有 kind 的结果、事件或 summary。批量逐条 ErrorInfo 属于结果内容,可出现在 stdout;命令启动或整次执行失败只写 stderr。
机器格式不经过 Rich 排版,不含 ANSI。text 模式遵守 NO_COLOR、--no-color,重定向时无动画;显示截断必须注明,export 永不静默截断。quiet 不吞掉结果或错误,verbose 不输出凭据。
| 退出码 | 条件 |
|---|---|
| 0 | 普通命令完整成功;标准验证结果全部 valid |
| 1 | 网络、协议、文件、落盘或回放执行失败 |
| 2 | 参数、配置、输入格式、缺少 extra、输入资源上限错误 |
| 3 | 数据/能力不可用、快照过期或就绪超时 |
| 10 | 验证或 BMP 分析完成且包含 invalid |
| 11 | 无 invalid,但有 notfound/unknown;BMP 为 no_invalid_evidence 或 indeterminate 也用 11 |
| 130 | 用户 Ctrl+C |
同一批次多个错误类别时顺序固定为 2、1、3,再考虑 10、11、0;中断优先 130。取消不能伪装成全部完成。BMP 的无反例不映射为标准验证成功 0。远程事件要求重同步时按 watch 契约恢复;不能自动恢复的有限查询报对应错误。
导出到文件先写同目录临时文件,完整写入后原子替换目标;覆盖已有文件须显式 --force。输出到 stdout 的中途失败返回非零且不能补造成功结尾。BrokenPipe 不打印内部 traceback,清理资源并返回执行失败。
启动和测试
提供 rpkiparrot 与 python -m rpkiparrot。未装 cli 时最小启动器返回 2 并提示安装 rpkiparrot[cli];核心 import 不导入 Typer。安装 cli 后 help/version/completion 无网络、数据库和其他 extras 依赖。
CLI 作为应用拥有 anyio.run 和任务组,serve 使用 Uvicorn asyncio 入口;退出顺序遵守 Client 契约。帮助从 Typer 公共接口生成,不访问其内部 Click 实现。
验收使用 Typer CliRunner 检查帮助、选项、输出与退出码,并用独立子进程检查实际入口、stdin、SIGINT、管道、缺少 extras 和落盘失败。命令输出与相同快照下的公共 API 对照。
Shell 补全
安装 cli extra 后,rpkiparrot --show-completion 输出当前 shell 的补全脚本,--install-completion 按 Typer 的公开机制安装。console 与 python -m rpkiparrot 入口都使用固定的 rpkiparrot 命令名生成合法脚本;补全绑定已安装的 rpkiparrot 命令,不承诺在 python -m rpkiparrot 后直接按 Tab 生效。生成补全和获取动态命令候选不读取来源数据或连接网络。
服务配置与启动
rpkiparrot --config client.toml serve [--host IP] [--port PORT] 使用 service extra,启动唯一的 asyncio/Uvicorn 拥有者。监听地址和端口默认取 [service],命令行显式值优先。覆盖地址后重新验证完整服务配置;不能把回环无认证配置直接改成公开监听。CLI 始终关闭 Uvicorn 的代理头自动重写,direct TLS 的证书和私钥实际传给监听器,proxy TLS 则由服务核对原始 TCP peer。信号触发后先停止准入并唤醒事件长轮询,Uvicorn 等待请求结束,再由 lifespan 关闭 Client 和数据库;阻塞工作需实际结束,Ctrl+C 返回 130。服务运行期间 stdout 不输出访问日志。
[service] 字段与公共 ServiceConfig 一致,但不能配置 Python token_provider;token_env = "MY_RPKI_SERVICE_TOKEN" 显式绑定环境变量,并配合 auth_mode = "bearer"。变量在服务 lifespan 启动时读取一次,缺失或非法会使启动失败;普通 TOML 加载不读取该秘密。不能把 token 值直接放入该表。TLS 路径相对 TOML 文件解析,配置和状态输出不包含 token。以下配置只绑定回环,仍使用 bearer;远程监听需另外选择 direct 或 proxy TLS。
[service]
host = "127.0.0.1"
port = 8323
auth_mode = "bearer"
token_env = "MY_RPKI_SERVICE_TOKEN"
录制与回放
rpkiparrot --rtr cache.example:323 --format json record --output session.jsonl --duration 60
rpkiparrot --config client.toml --format json record --source-id cache-a --output selected.jsonl
rpkiparrot --format json replay session.jsonl --at 2026-01-01T00:00:00Z
record 使用公共 Recorder 与 RtrSession,仅运行选定的 RTR 会话;配置里恰有一个 RTR 来源时可省略 --source-id,多个时必须明确选择。其他来源和持久化后端不启动。输出文件必须不存在,不能覆盖旧录制;文件准备失败时不开始连接。默认录制 60 秒,--duration 必须为有限正数,--watch 表示持续到中断,两者互斥;--watch 只接受 text/jsonl。有限录制先关闭会话与拥有的流,再完成录制线程和 trailer。成功 stdout 为 kind=recording、mode=diagnostic、snapshot_id=null,包含 RecorderStatus、已确认完整 EOD 数及最后来源状态,不能当作在线可信快照。源的最终网络/协议错误或不完整录制返回 1,仅 stderr 输出操作错误;已写文件保留供诊断。Ctrl+C 返回 130,不补造成功 summary,不完整证据仍可保留。 record 验收的是录制完整性,不要求来源就绪:合法 No Data Available,或录制时段结束前尚未收到 EOD 且未触发查询超时,可在录制完整、无最终操作错误时返回 0,同时明确 acknowledged_updates=0 和最后来源状态。真正的 query_timeout 或传输错误仍返回 1。
replay 必须给出带时区的 --at,可放在全局或子命令位置;两处同时给出时必须一致。它仅读取明确 PATH;可用 --config 取得 limits 和输出默认值,不打开配置里的数据库或启动来源。不能与 --rtr、--json-file、--snapshot、--database 或 --remote 混用。正常 stdout 为 kind=replay、mode=offline,data 是公共 ReplayReport;--max-events 与 --max-report-bytes 可进一步限制报告大小。报告完整且与状态机匹配返回 0;已生成的差异、明确不完整或报告截断诊断仍输出报告,返回 1。格式/参数错误仅写 stderr 并返回 2;--allow-incomplete 允许检查有限前缀,不把它改成成功。完整录制与 matched=true 的边界见录制和回放契约。