117 Commits

Author SHA1 Message Date
daf0764cd0 20260720 规范化 CCR Manifest location 导出 2026-07-20 16:24:47 +08:00
06f4b086b4 20260718 Explorer 接入 VRP 检索查询 2026-07-20 10:05:00 +08:00
6d9eedd680 20260718 新增查询服务 VRP 检索接口 2026-07-20 10:04:43 +08:00
e80fba44c3 20260718 修复 Explorer 接口列表键值冲突 2026-07-18 10:46:25 +08:00
c48dc04909 20260718 格式化查询服务代码 2026-07-18 10:40:51 +08:00
a5902c0a5e 20260718 重构 RPKI Explorer 查询界面 2026-07-18 10:38:38 +08:00
a77982deaa 20260717 rpctl接入rpki-prover实验控制 2026-07-17 18:10:06 +08:00
4865d3698d 20260716 澄清CA验证上下文摘要命名 2026-07-16 22:54:59 +08:00
59623b449b 20260716 增加verification-only缓存正确性复核 2026-07-16 19:08:16 +08:00
7aac4855bd 20260715 add configurable max CA depth 2026-07-16 09:29:57 +08:00
9f4f4cf069 20260713 unify installer scripts and RTR registration 2026-07-13 12:46:40 +08:00
b66b425512 20260711 对齐EE证书负例与profile校验 2026-07-12 08:09:59 +08:00
363e69d11d 20260711 修正ROA缓存projection存储测试 2026-07-12 07:40:28 +08:00
da6e2e515d 20260711 修正证书路径测试profile夹具 2026-07-12 07:24:45 +08:00
4a64fa7900 20260711 补充P1缓存与证书profile回归 2026-07-12 07:15:32 +08:00
1ce897dbff 20260711 完善资源证书基础profile校验 2026-07-12 07:04:16 +08:00
08281fa231 20260711 收紧验证缓存复用边界 2026-07-12 06:53:59 +08:00
60796299b0 secure validation cache reuse 2026-07-11 21:57:46 +08:00
6de0bced1c add role-aware resource certificate profile 2026-07-11 21:40:52 +08:00
8e37ef48cb 20260711 验证TAL信任锚自签名 2026-07-11 21:13:52 +08:00
8613cfae60 20260710 完善安装包镜像溯源元数据 2026-07-10 18:13:33 +08:00
f3934c3bf8 20260710 Docker构建固定本地基础镜像 2026-07-10 17:36:55 +08:00
00613afdde 20260710 Docker构建复用本地基础镜像 2026-07-10 16:41:29 +08:00
ea6ac27b8d 20260710 安装包重建提交标识镜像 2026-07-10 16:00:51 +08:00
de4f40eb2c 20260710 rpctl统一实验标识为exp-id 2026-07-10 15:25:37 +08:00
87607a3991 20260710 收敛Docker安装包Compose模板 2026-07-10 14:44:43 +08:00
96d82957a2 20260710 增加四RP本地控制台rpctl 2026-07-10 12:39:24 +08:00
63ac3ee254 20260707 支持本地HTTP root证书 2026-07-07 23:04:44 +08:00
b0f76738e5 20260707 通用化Docker安装包模板命名 2026-07-07 11:10:21 +08:00
c00d9390dc 20260707 安装包默认清理daemon临时目录 2026-07-07 10:26:30 +08:00
a5b194ce35 20260701 add validation-update-03 VRS mode 2026-07-01 20:28:25 +08:00
a501cfb709 20260701 installer接入RTR监控目录 2026-07-01 12:06:49 +08:00
f8988b18cd split metrics installer image 2026-07-01 00:53:31 +08:00
a67640db83 add rtr report metrics 2026-06-30 18:28:52 +08:00
d5270308d6 fix installer image retag 2026-06-30 17:24:36 +08:00
6c45cc94f3 add multi-arch docker installer 2026-06-30 16:18:41 +08:00
371c819126 drop tracked pycache files 2026-06-29 23:25:19 +08:00
a329fed3df fix lifecycle delta count meta 2026-06-29 23:25:19 +08:00
860bad2113 add lifecycle run state 2026-06-29 23:25:19 +08:00
0610a8291c preserve tmp mountpoint on reset 2026-06-29 18:37:14 +08:00
b83acb281b fix tmp failure isolation 2026-06-29 18:37:14 +08:00
8d15188ea8 fix periodic snapshot upgrade env merge 2026-06-29 16:59:00 +08:00
4ca08a12a4 fix periodic snapshot upgrade edge cases 2026-06-29 16:53:10 +08:00
fe8d4fcb14 add periodic snapshot reset 2026-06-29 16:47:47 +08:00
f9f91f072a 20260629 修复ARM监控fallback面板零值显示 2026-06-29 13:23:44 +08:00
4b98a9cd37 align arm64 dashboards 2026-06-29 12:39:27 +08:00
6d9646de2c 20260628 增加三层缓存消融实验脚本 2026-06-29 09:14:52 +08:00
9f98ac6394 20260627 ARM64安装包内置监控镜像 2026-06-27 16:47:12 +08:00
a87a73559b 20260627 完成ARM64完整安装包和运维脚本 2026-06-27 12:34:29 +08:00
79a77f6f6e 20260626 ARM64 Docker Compose部署与live TA首轮刷新 2026-06-27 11:08:39 +08:00
f1a73bd2d1 20260626_2 优化child证书缓存并修复远端发布 2026-06-26 18:52:27 +08:00
9e339e63e7 20260626 优化PP缓存索引与证书缓存长尾 2026-06-26 07:19:11 +08:00
af25316d68 20260624 修复future notBefore PP缓存并固化231发布 2026-06-24 13:36:39 +08:00
adb68fd469 20260624 优化ROA cache lookup长尾 2026-06-24 09:50:23 +08:00
8c4a677ffa 20260623 细化ROA cache CRL gate 2026-06-24 00:05:14 +08:00
574e40a4d4 20260623 optimize rpki explorer ui interactions 2026-06-23 15:47:50 +08:00
4f41fbe04e 20260623 增加RP差异趋势面板 2026-06-23 11:51:29 +08:00
6ab044480a 20260623 迁移231并固定相位调度 2026-06-23 11:37:36 +08:00
4e37b96aff update inter-rp repo sync metrics 2026-06-22 18:01:57 +08:00
92d184681b 20260622 接入Routinator仓库同步指标 2026-06-22 16:27:19 +08:00
4546d90c33 20260622 恢复Routinator inter-RP监控 2026-06-22 15:48:31 +08:00
61d3e636ae 20260620 mmap PP cache index消除RocksDB全量扫描尖峰 2026-06-21 12:46:23 +08:00
184d3cb95b 20260620 优化delta长尾控制面和PP cache快路径 2026-06-20 15:31:20 +08:00
bd266ef2a5 20260618 优化PP cache hit child恢复 2026-06-18 18:05:27 +08:00
d4d227ce60 20260618 增加PP缓存命中路径性能埋点 2026-06-18 16:33:37 +08:00
261d652123 20260618 修复远端231发布验证流程 2026-06-18 11:31:55 +08:00
b6344074ce 20260618 增加远端231原地发布脚本 2026-06-18 09:48:18 +08:00
ad2a25aede 20260617 发布点级验证缓存复用 2026-06-17 23:49:53 +08:00
68006467f7 20260617 增加状态DB监控指标和Grafana面板 2026-06-17 20:45:37 +08:00
56f0d10dc6 20260617 增加RPKI Explorer前端 2026-06-17 17:29:20 +08:00
6ef2c98890 20260616 增加产品UI查询服务 2026-06-17 10:16:04 +08:00
4e6bd687db 20260612 优化CIR在线累计恢复性能 2026-06-12 14:26:49 +08:00
4047214ddf 20260609 CIR区分fresh和cached验证输入 2026-06-11 19:13:48 +08:00
74012c686d 20260609 增加三RP持续对比监控 2026-06-09 17:44:30 +08:00
d0224d53f6 20260608 默认rsync scope切到module-root 2026-06-08 13:23:16 +08:00
8e6e2f1318 20260607 完成transport预热、roa cache和finalize长尾优化 2026-06-07 20:35:47 +08:00
4045f9a3a5 20260605 ROA增量验证和rsync host失败短路 2026-06-06 15:16:38 +08:00
e597c7c124 20260605 移除audit rule index和DB trace 2026-06-05 16:02:25 +08:00
9579f65501 20260601 sequence triage支持独立时间序列 2026-06-01 23:41:17 +08:00
902f3ba889 20260601 sequence triage按host聚合URI 2026-06-01 21:51:38 +08:00
938ef53173 20260601 sequence triage增加聚合视图 2026-06-01 20:55:39 +08:00
ae00e676d7 20260601 精简sequence triage并增加churn统计 2026-06-01 19:55:07 +08:00
bf8c924326 20260601 优化sequence远端产物拉取 2026-06-01 13:51:08 +08:00
00d7109503 20260531 拆分sequence triage工具并修复远端产物拉取 2026-06-01 11:01:42 +08:00
a29fe266a4 20260530 行为保持源码治理重构 2026-05-30 17:19:42 +08:00
2154870a43 20260529 实现序列三明治异常检测 2026-05-30 07:15:20 +08:00
9f7981e117 20260529 修复CIR输入与拒绝审计语义 2026-05-29 17:32:40 +08:00
57c23f19aa 20260527 增加repo同步监控面板指标 2026-05-27 15:47:09 +08:00
7e1c24fcc3 20260526 增加持续soak监控与本地回放工具 2026-05-26 18:02:40 +08:00
cda7fdb135 20260521 三RP性能对比支持rpki-client p4 2026-05-23 17:27:42 +08:00
fcd0bac070 20260519_2 收紧CCR/CIR四文件分诊 2026-05-20 07:07:13 +08:00
8c6fb44352 20260518 增加三RP性能对比测试驱动 2026-05-19 22:13:34 +08:00
615f8709af 20260514 rsync默认限定发布点scope 2026-05-15 17:17:41 +08:00
137b3516d0 20260512 完成CCR CIR行为差异实验能力 2026-05-13 05:11:17 +08:00
f2fbb20a29 20260511 CIR V3 trust anchors self-contained 2026-05-11 18:12:41 +08:00
51e483d924 20260510 增加strict策略模式并隔离多TA失败 2026-05-11 11:32:17 +08:00
265b6f65d0 20260509 add portable soak package 2026-05-09 19:03:23 +08:00
c6b408c0f9 20260507_2 增加CCR/CIR差异定位链路 2026-05-09 10:02:47 +08:00
752e746b97 20260506_2 为CIR增加reject list基础能力 2026-05-06 20:53:57 +08:00
51663a9410 20260506 清理废弃bundle代码并收敛ccr compare view 2026-05-06 16:49:23 +08:00
f843eedda9 20260504 优化phase2 finalize调度长尾 2026-05-06 12:05:02 +08:00
b3b44d50c6 20260501 修复CurrentRepoIndex完整性并优化replay guard 2026-05-03 08:26:18 +08:00
ad61caf271 20260428 daemon运行化和delta复用warning修复 2026-04-29 08:13:10 +08:00
3b2a160c5c 20260428 降低all5 CIR replay内存峰值 2026-04-28 09:58:43 +08:00
0295fd3262 20260427_5 支持all5 CIR replay多TAL 2026-04-28 00:20:12 +08:00
e2901df3ac 20260427_4 默认禁用work-db BlobDB 2026-04-27 22:08:33 +08:00
26aec5ff35 20260427_3 增强db_stats归因work-db体积 2026-04-27 18:00:34 +08:00
87275b5c57 20260427_2 移除parallel phase开关,默认全量并行 2026-04-27 17:13:32 +08:00
eaa375c5ec 20260427 拆分独立repo-bytes.db数据库文件,cir materialize 移除旧兼容 2026-04-27 16:01:33 +08:00
944ea6ca00 20260421 优化内存布局,降低内存峰值需求压低到4.5GB左右,在4c8g 机器上,稳定跑snapshot + delta 2026-04-21 15:21:25 +08:00
542bd7be80 20260420_2 完成输出report和ccr优化,snapshot耗时优化到70多秒 2026-04-20 18:28:59 +08:00
f6a601e16c 20260418_2 phase2 并行优化 mix quick 耗时105/48秒 2026-04-19 00:08:29 +08:00
417c82bef6 20260418 优化去掉冗余存储repo object,mix quick最快170s 2026-04-18 14:10:47 +08:00
f485786470 20260147 迭代优化全量测试和覆盖率测试,时间从325秒降低到90+秒,覆盖率维持在90% 2026-04-17 17:18:05 +08:00
224ae10052 20260416_2 并行优化phase1后进行snapshot fast path优化,通过四方面关键优化技术消除了验证主链路上snapshot构建是的db访问性能热点问题,性能热点转移到ROA验证处理本身,目前APNIC+ARIN全量同步从500秒压缩到212秒,离rpkclient 112秒差距变小 2026-04-17 14:58:47 +08:00
38421b1ae7 20260415_2 支持多个RIR并行混合执行,APNIC+ARIN运行1 snapshot + 1 delta,对比rpki-client,snapshot比rpki-client慢4分钟(398vs137),输出未收敛,delta时输出收敛(348vs181),评估应该是正确性没有问题,下一步进一步优化性能 2026-04-16 11:33:52 +08:00
585c41b83b 20260413_2 并行化架构优化第一阶段,apnic 串行98s->并行74s,传输层任务并行,同repo内发布点串行 2026-04-15 09:53:11 +08:00
382 changed files with 110339 additions and 18919 deletions

17
.dockerignore Normal file
View File

@@ -0,0 +1,17 @@
target/
.git/
.gitignore
perf.*
**/* copy.excalidraw
ui/rpki-explorer/node_modules/
ui/rpki-explorer/dist/
ui/rpki-explorer/playwright-report/
ui/rpki-explorer/test-results/
ui/rpki-explorer/.vite/
deploy/arm64-compose/.env
target/
*.profraw
*.profdata
*.tar
*.tar.gz
*.zip

6
.gitignore vendored
View File

@@ -1,3 +1,9 @@
target/
Cargo.lock
perf.*
specs/* copy.excalidraw
ui/rpki-explorer/node_modules/
ui/rpki-explorer/dist/
ui/rpki-explorer/playwright-report/
ui/rpki-explorer/test-results/
ui/rpki-explorer/.vite/

View File

@@ -21,14 +21,15 @@ ring = "0.17.14"
x509-parser = { version = "0.18.0", features = ["verify"] }
url = "2.5.8"
serde = { version = "1.0.218", features = ["derive"] }
serde_json = "1.0.140"
serde_json = { version = "1.0.140", features = ["raw_value"] }
toml = "0.8.20"
rocksdb = { version = "0.22.0", optional = true, default-features = false, features = ["lz4"] }
serde_cbor = "0.11.2"
memmap2 = "0.9.10"
roxmltree = "0.20.0"
quick-xml = "0.37.2"
uuid = { version = "1.7.0", features = ["v4"] }
reqwest = { version = "0.12.12", default-features = false, features = ["blocking", "rustls-tls"] }
reqwest = { version = "0.12.12", default-features = false, features = ["blocking", "rustls-tls", "gzip", "brotli", "deflate"] }
pprof = { version = "0.14.1", optional = true, features = ["flamegraph", "prost-codec"] }
flate2 = { version = "1.0.35", optional = true }
tempfile = "3.16.0"

View File

@@ -26,3 +26,38 @@ cargo install cargo-llvm-cov --locked
cargo llvm-cov --fail-under-lines 90
```
默认会复用现有插桩产物,不会先 clean。需要强制全量重编译时:
```
COVERAGE_FORCE_CLEAN=1 ./scripts/coverage.sh
```
说明:
- 默认行为适合本地重复确认覆盖率,避免每次都重编译整套插桩目标;
- 默认还会设置 `RPKI_SKIP_HEAVY_SCRIPT_REPLAY_TESTS=1`,跳过会拉起 shell replay pipeline 的重型集成测试,避免 coverage 期间额外触发 `target/release` 构建;
- 默认还会设置 `RPKI_SKIP_HEAVY_BLACKBOX_TESTS=1`,跳过更慢的 blackbox CLI / CIR record 脚本测试,进一步降低日常 coverage 成本;
- 默认还会设置 `RPKI_SKIP_HEAVY_CRYPTO_TESTS=1`,跳过需要大量 OpenSSL 生成证书/CRL 的重型密码学测试,进一步压缩日常 coverage 时长;
- 如需把这批脚本回放测试也纳入 coverage,可显式关闭该开关:
```
RPKI_SKIP_HEAVY_SCRIPT_REPLAY_TESTS=0 ./scripts/coverage.sh
```
如需连同第二批 blackbox 测试一起跑:
```
RPKI_SKIP_HEAVY_BLACKBOX_TESTS=0 ./scripts/coverage.sh
```
如需连同重型 OpenSSL 证书路径测试一起跑:
```
RPKI_SKIP_HEAVY_CRYPTO_TESTS=0 ./scripts/coverage.sh
```
- replay 脚本现在也支持通过环境变量注入现成二进制,避免找不到二进制时自动 `cargo build --release`:
- `RPKI_BIN`
- `CIR_MATERIALIZE_BIN`
- `CIR_EXTRACT_INPUTS_BIN`
- `CCR_TO_COMPARE_VIEWS_BIN`
- `COVERAGE_FORCE_CLEAN=1` 适合需要完全从零重建插桩目标时使用。

View File

@@ -0,0 +1,108 @@
# ours RP Docker installer configuration
# `build_docker_installer_package.sh` rewrites package-specific placeholders
# when producing amd64/arm64 release packages.
# 中文说明见 docs/README.zh-CN.md。English guide: docs/README.en.md
# Package metadata and architecture guardrails.
PACKAGE_ARCH=__PACKAGE_ARCH__
PACKAGE_PLATFORM=__PACKAGE_PLATFORM__
ALLOW_CROSS_ARCH=0
# Compose project name.
COMPOSE_PROJECT_NAME=ours-rp-__PACKAGE_ARCH__-installer
# Runtime image rebuilt from the current source commit while packaging.
RPKI_IMAGE=__RUNTIME_IMAGE__
RPKI_PLATFORM=__PACKAGE_PLATFORM__
# Metrics image rebuilt from the current source commit while packaging.
METRICS_IMAGE=__METRICS_IMAGE__
METRICS_PLATFORM=__PACKAGE_PLATFORM__
# Restart policy for the soak container. Production default keeps the daemon alive.
# For finite acceptance tests such as MAX_RUNS=3, set SOAK_RESTART_POLICY=no to avoid an extra restarted run.
SOAK_RESTART_POLICY=unless-stopped
# Host-side persistent data directory. All state/runs/logs/monitoring data are bind-mounted here.
HOST_DATA_DIR=__HOST_DATA_DIR__
# RIR list. Options: afrinic,apnic,arin,lacnic,ripe
RIRS=afrinic,apnic,arin,lacnic,ripe
# Negative MAX_RUNS means keep running forever. Default production interval is 10 minutes.
MAX_RUNS=-1
INTERVAL_SECS=600
RETAIN_RUNS=100
# TAL/TA input mode:
# file-with-ta: use packaged fixture TAL + TA only.
# file-live-ta: use packaged fixture TAL; snapshot waits for live TA refresh, delta refreshes TA in background.
# url: pass TAL URL to child process.
TAL_INPUT_MODE=file-live-ta
LIVE_TA_REFRESH_BEFORE_SNAPSHOT=1
LIVE_TA_REFRESH_CONNECT_TIMEOUT_SECS=15
LIVE_TA_REFRESH_MAX_TIME_SECS=120
# Sync and runtime behavior.
RSYNC_SCOPE=module-root
DISABLE_COMPETING_RPS=0
RUN_ROOT=/var/lib/ours-rp
DB_DIR=/var/lib/ours-rp/state/db
RSYNC_MIRROR_ROOT=/var/lib/ours-rp/state/rsync-mirror
# 每轮完成并复制出正式 run 产物后清理 tmp/daemon-run_*,避免长跑占满磁盘。
CLEAN_TMP_AFTER_RUN=1
OUTPUT_COMPACT_REPORT=1
ALLOW_RSYNC_MIRROR_REUSE=1
FAILURE_SNAPSHOT_RESET=1
# Periodic snapshot reset of active state DB.
# 0: keep existing behavior.
# 1: after one successful snapshot, allow at most N successful delta runs;
# the next run is forced to snapshot and active state/db is rebuilt from empty.
# Lifecycle run state is persisted independently at:
# ${HOST_DATA_DIR}/state/run-lifecycle-state.json
# It is not affected by run retention or state/db reset.
PERIODIC_SNAPSHOT_RESET=0
PERIODIC_SNAPSHOT_MAX_DELTAS=100
DB_STATS_EXACT_EVERY=0
# Resource certificate validation mode passed to the rpki child process.
# Options: validation-update-03 | rfc6487
RESOURCE_VALIDATION_MODE=validation-update-03
# Validation and performance options aligned with current optimized soak defaults.
ENABLE_CHILD_CERTIFICATE_VALIDATION_CACHE=1
RPKI_ANALYZE=1
RPKI_EXTRA_ARGS="--enable-transport-request-prefetch --enable-publication-point-validation-cache --enable-roa-validation-cache --parallel-max-repo-sync-workers-global 4 --parallel-phase2-object-workers 4 --memory-trim-after-validation"
# Progress logs.
RPKI_PROGRESS_LOG=1
RPKI_PROGRESS_SLOW_SECS=20
RPKI_PROGRESS_STAGE_FRESH_SLOW_MS=2000
RPKI_PROGRESS_PP_CONTROL_SLOW_MS=200
RPKI_PROGRESS_PP_CACHE_SLOW_MS=100
RPKI_PROGRESS_CONTROL_LOOP_SLOW_MS=2000
# Metrics sidecar.
METRICS_INSTANCE=__PACKAGE_ARCH__-installer
METRICS_PORT=9556
METRICS_POLL_SECS=10
# Optional external RTR report directory produced by a separately deployed RTR service.
# Default points to an installer-managed empty fallback directory. To enable real RTR
# metrics, set this to the host-side report directory, for example:
# RTR_REPORT_DIR=/root/rpki/report
RTR_REPORT_DIR=__HOST_DATA_DIR__/empty-rtr-report
RTR_REPORT_CONTAINER_DIR=/var/lib/ours-rp/rtr-report
# Prometheus / Grafana.
# Monitor images are packaged as docker-save archives and loaded by install.sh.
MONITOR_PLATFORM=__PACKAGE_PLATFORM__
PROMETHEUS_IMAGE=prom/prometheus:v2.55.1
GRAFANA_IMAGE=grafana/grafana:11.3.1
PROMETHEUS_PORT=9090
PROMETHEUS_RETENTION=7d
GRAFANA_PORT=3000
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=admin
# First snapshot waiting timeout used by start.sh.
FIRST_RUN_WAIT_TIMEOUT_SECS=7200

View File

@@ -0,0 +1,93 @@
services:
ours-rp-soak:
image: ${RPKI_IMAGE:?RPKI_IMAGE is required}
platform: ${RPKI_PLATFORM:?RPKI_PLATFORM is required}
container_name: ${COMPOSE_PROJECT_NAME:-ours-rp-package-installer}-soak
env_file:
- ../.env
environment:
PACKAGE_ROOT: /opt/ours-rp
ENV_FILE: /opt/ours-rp/.env
RUN_ROOT: /var/lib/ours-rp
BIN_DIR: /opt/ours-rp/bin
FIXTURE_DIR: /opt/ours-rp/fixtures
volumes:
- ../.env:/opt/ours-rp/.env:ro
- ${HOST_DATA_DIR:-/var/lib/ours-rp-package-installer}/state:/var/lib/ours-rp/state
- ${HOST_DATA_DIR:-/var/lib/ours-rp-package-installer}/runs:/var/lib/ours-rp/runs
- ${HOST_DATA_DIR:-/var/lib/ours-rp-package-installer}/logs:/var/lib/ours-rp/logs
- ${HOST_DATA_DIR:-/var/lib/ours-rp-package-installer}/tmp:/var/lib/ours-rp/tmp
restart: ${SOAK_RESTART_POLICY:-unless-stopped}
profiles:
- core
artifact-metrics:
image: ${METRICS_IMAGE:?METRICS_IMAGE is required}
platform: ${METRICS_PLATFORM:?METRICS_PLATFORM is required}
container_name: ${COMPOSE_PROJECT_NAME:-ours-rp-package-installer}-artifact-metrics
env_file:
- ../.env
environment:
RPKI_METRICS_RTR_REPORT_DIR: ${RTR_REPORT_CONTAINER_DIR:-/var/lib/ours-rp/rtr-report}
command:
- /opt/ours-rp/bin/rpki_artifact_metrics
- --run-root
- /var/lib/ours-rp
- --listen
- 0.0.0.0:9556
- --poll-secs
- ${METRICS_POLL_SECS:-10}
- --instance
- ${METRICS_INSTANCE:-package-installer}
ports:
- "${METRICS_PORT:-9556}:9556"
volumes:
- ${HOST_DATA_DIR:-/var/lib/ours-rp-package-installer}/state:/var/lib/ours-rp/state:ro
- ${HOST_DATA_DIR:-/var/lib/ours-rp-package-installer}/runs:/var/lib/ours-rp/runs:ro
- ${HOST_DATA_DIR:-/var/lib/ours-rp-package-installer}/logs:/var/lib/ours-rp/logs:ro
- ${RTR_REPORT_DIR:-/var/lib/ours-rp-package-installer/empty-rtr-report}:${RTR_REPORT_CONTAINER_DIR:-/var/lib/ours-rp/rtr-report}:ro
restart: unless-stopped
profiles:
- sidecar
prometheus:
image: ${PROMETHEUS_IMAGE:-prom/prometheus:v2.55.1}
platform: ${MONITOR_PLATFORM:?MONITOR_PLATFORM is required}
container_name: ${COMPOSE_PROJECT_NAME:-ours-rp-package-installer}-prometheus
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=${PROMETHEUS_RETENTION:-7d}
- --web.enable-lifecycle
depends_on:
- artifact-metrics
user: "0:0"
ports:
- "${PROMETHEUS_PORT:-9090}:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ${HOST_DATA_DIR:-/var/lib/ours-rp-package-installer}/prometheus:/prometheus
restart: unless-stopped
profiles:
- monitor
grafana:
image: ${GRAFANA_IMAGE:-grafana/grafana:11.3.1}
platform: ${MONITOR_PLATFORM:?MONITOR_PLATFORM is required}
container_name: ${COMPOSE_PROJECT_NAME:-ours-rp-package-installer}-grafana
depends_on:
- prometheus
user: "0:0"
ports:
- "${GRAFANA_PORT:-3000}:3000"
environment:
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin}
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-admin}
GF_USERS_ALLOW_SIGN_UP: "false"
volumes:
- ${HOST_DATA_DIR:-/var/lib/ours-rp-package-installer}/grafana:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
restart: unless-stopped
profiles:
- monitor

View File

@@ -0,0 +1,761 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 6,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "ours_rp_publication_points",
"legendFormat": "publication points",
"refId": "A"
}
],
"title": "Publication Points",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 6,
"x": 6,
"y": 0
},
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "ours_rp_repo_sync_phase_count{phase=\"rrdp_ok\"}",
"legendFormat": "rrdp ok",
"refId": "A"
}
],
"title": "RRDP OK Points",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 6,
"x": 12,
"y": 0
},
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "sum(ours_rp_repo_sync_phase_count{phase=\"rrdp_failed_rsync_ok\"}) or vector(0)",
"legendFormat": "fallback",
"refId": "A"
}
],
"title": "Rsync Fallback Points",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 6,
"x": 18,
"y": 0
},
"id": 4,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "ours_rp_repo_terminal_state_count{terminal_state=\"failed_no_cache\"}",
"legendFormat": "failed no cache",
"refId": "A"
}
],
"title": "Failed No Cache Points",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 4
},
"id": 5,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_run_stage_duration_seconds{stage=\"repo_sync_total\"}",
"legendFormat": "repo sync total",
"refId": "A"
},
{
"expr": "ours_rp_run_stage_duration_seconds{stage=\"rrdp_download_total\"}",
"legendFormat": "rrdp download",
"refId": "B"
},
{
"expr": "ours_rp_run_stage_duration_seconds{stage=\"rsync_download_total\"}",
"legendFormat": "rsync download",
"refId": "C"
}
],
"title": "Repo Sync Download Durations",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 12,
"w": 12,
"h": 8
},
"id": 6,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_repo_sync_phase_count",
"legendFormat": "{{phase}}",
"refId": "A"
}
],
"title": "Repo Sync Phase Counts",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"gridPos": {
"x": 12,
"y": 12,
"w": 12,
"h": 8
},
"id": 7,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_repo_sync_phase_count{phase=\"rrdp_failed_rsync_ok\"}",
"legendFormat": "rrdp failed, rsync ok",
"refId": "A"
},
{
"expr": "ours_rp_repo_sync_phase_count{phase=\"rrdp_failed_rsync_failed\"}",
"legendFormat": "rrdp failed, rsync failed",
"refId": "B"
},
{
"expr": "ours_rp_repo_terminal_state_count{terminal_state=\"failed_no_cache\"}",
"legendFormat": "failed no cache",
"refId": "C"
},
{
"expr": "ours_rp_tree_instances{state=\"failed\"}",
"legendFormat": "tree failed",
"refId": "D"
}
],
"title": "Repo Failure / Fallback Counts",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "s"
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 20,
"w": 12,
"h": 8
},
"id": 8,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_repo_sync_phase_duration_seconds_total{phase=\"rrdp_failed_rsync_ok\"}",
"legendFormat": "rsync fallback duration",
"refId": "A"
},
{
"expr": "ours_rp_repo_sync_phase_duration_seconds_total{phase=\"rrdp_failed_rsync_failed\"}",
"legendFormat": "failed duration",
"refId": "B"
},
{
"expr": "ours_rp_repo_terminal_state_duration_seconds_total{terminal_state=\"failed_no_cache\"}",
"legendFormat": "failed no cache duration",
"refId": "C"
}
],
"title": "Repo Failure / Fallback Durations",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "none"
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 29,
"w": 12,
"h": 9
},
"id": 9,
"options": {
"showHeader": true,
"cellHeight": "sm",
"footer": {
"show": false,
"reducer": [
"sum"
],
"countRows": false,
"fields": ""
}
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "ours_rp_rrdp_rsync_failed_repository_duration_seconds",
"format": "table",
"instant": true,
"legendFormat": "",
"refId": "A"
}
],
"title": "RRDP + Rsync Failed Repositories",
"type": "table",
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"job": true,
"terminal_state": true,
"rank": true,
"transport": true,
"__name__": true,
"publication_points": true,
"instance": true,
"repo_id": true,
"pp_id": true,
"exported_instance": true,
"rp": true,
"source": true
},
"indexByName": {
"Time": 0,
"host": 1,
"phase": 2,
"uri": 3,
"Value": 4
},
"renameByName": {
"Value": "duration"
}
}
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "none"
},
"overrides": []
},
"gridPos": {
"x": 12,
"y": 29,
"w": 12,
"h": 9
},
"id": 11,
"options": {
"showHeader": true,
"cellHeight": "sm",
"footer": {
"show": false,
"reducer": [
"sum"
],
"countRows": false,
"fields": ""
}
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "topk(20, ours_rp_top_repository_sync_duration_seconds)",
"format": "table",
"instant": true,
"legendFormat": "",
"refId": "A"
}
],
"title": "Top 20 Repositories by Sync Duration",
"type": "table",
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"job": true,
"terminal_state": true,
"__name__": true,
"publication_points": true,
"instance": true,
"repo_id": true,
"phase": true,
"pp_id": true,
"exported_instance": true,
"rp": true,
"source": true
},
"indexByName": {
"Time": 0,
"host": 1,
"rank": 2,
"transport": 3,
"uri": 4,
"Value": 5
},
"renameByName": {
"Value": "value"
}
}
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "none"
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 38,
"w": 24,
"h": 9
},
"id": 10,
"options": {
"showHeader": true,
"cellHeight": "sm",
"footer": {
"show": false,
"reducer": [
"sum"
],
"countRows": false,
"fields": ""
}
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "topk(20, ours_rp_top_publication_point_object_count)",
"format": "table",
"instant": true,
"legendFormat": "",
"refId": "A"
}
],
"title": "Top Publication Points by Objects",
"type": "table",
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"job": true,
"__name__": true,
"publication_points": true,
"instance": true,
"repo_id": true,
"phase": true,
"pp_id": true,
"exported_instance": true,
"rp": true,
"source": true
},
"indexByName": {
"Time": 0,
"host": 1,
"rank": 2,
"terminal_state": 3,
"transport": 4,
"uri": 5,
"Value": 6
},
"renameByName": {}
}
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"description": "Per-repository sync success in the latest successful run; 1 means successful, 0 means failed or failed_no_cache.",
"fieldConfig": {
"defaults": {
"unit": "bool"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 47
},
"id": 12,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"expr": "ours_rp_repository_sync_success",
"legendFormat": "{{host}} {{repo_id}}",
"refId": "A"
}
],
"title": "Repository Sync Success by Repo",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"description": "Per-repository total sync duration aggregated from publication point repo_sync_duration_ms.",
"fieldConfig": {
"defaults": {
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 55
},
"id": 13,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"expr": "ours_rp_repository_sync_duration_seconds{stat=\"sum\"}",
"legendFormat": "{{host}} {{repo_id}}",
"refId": "A"
}
],
"title": "Repository Sync Duration by Repo",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"description": "Per-repository downloaded bytes attributed from report.json downloads events.",
"fieldConfig": {
"defaults": {
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 63
},
"id": 14,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"expr": "ours_rp_repository_download_bytes",
"legendFormat": "{{host}} {{repo_id}}",
"refId": "A"
}
],
"title": "Repository Download Bytes by Repo",
"type": "timeseries"
}
],
"refresh": "5s",
"schemaVersion": 40,
"tags": [
"ours-rp",
"rpki",
"soak",
"repo-sync"
],
"templating": {
"list": []
},
"time": {
"from": "now-30m",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "Ours RP Repo Sync",
"uid": "ours-rp-repo-sync",
"version": 3,
"weekStart": ""
}

View File

@@ -0,0 +1,666 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"id": 1,
"title": "RTR Metrics Enabled",
"type": "stat",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 4,
"w": 4,
"x": 0,
"y": 0
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"expr": "max(ours_rp_rtr_metrics_enabled)",
"legendFormat": "enabled",
"refId": "A",
"instant": true
}
]
},
{
"id": 2,
"title": "Refresh Success",
"type": "stat",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 4,
"w": 5,
"x": 4,
"y": 0
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"expr": "max(ours_rp_rtr_source_refresh_status{status=\"success\"})",
"legendFormat": "success",
"refId": "A",
"instant": true
}
]
},
{
"id": 3,
"title": "Consecutive Failures",
"type": "stat",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 4,
"w": 5,
"x": 9,
"y": 0
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"expr": "max(ours_rp_rtr_source_refresh_consecutive_failures)",
"legendFormat": "failures",
"refId": "A",
"instant": true
}
]
},
{
"id": 4,
"title": "Last Success Age",
"type": "stat",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 4,
"w": 5,
"x": 14,
"y": 0
},
"fieldConfig": {
"defaults": {
"unit": "s",
"decimals": 0,
"min": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"expr": "max(ours_rp_rtr_source_last_success_age_seconds)",
"legendFormat": "age",
"refId": "A",
"instant": true
}
]
},
{
"id": 5,
"title": "RTR RSS",
"type": "stat",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 4,
"w": 5,
"x": 19,
"y": 0
},
"fieldConfig": {
"defaults": {
"unit": "bytes",
"decimals": 0,
"min": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"expr": "max(ours_rp_rtr_process_rss_bytes)",
"legendFormat": "rss",
"refId": "A",
"instant": true
}
]
},
{
"id": 6,
"title": "Data Quality Totals",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 4
},
"fieldConfig": {
"defaults": {
"unit": "short",
"min": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_rtr_data_quality_items{stage=~\"ccr_input|before_slurm|after_slurm\",type=\"total\"}",
"legendFormat": "{{stage}} total",
"refId": "A"
},
{
"expr": "ours_rp_rtr_data_quality_items{stage=\"after_slurm\",type=\"vrp\"}",
"legendFormat": "after_slurm vrp",
"refId": "B"
},
{
"expr": "ours_rp_rtr_data_quality_items{stage=\"after_slurm\",type=\"aspa\"}",
"legendFormat": "after_slurm aspa",
"refId": "C"
}
]
},
{
"id": 7,
"title": "SLURM Filters / Assertions",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 4
},
"fieldConfig": {
"defaults": {
"unit": "short",
"min": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_rtr_slurm_filters",
"legendFormat": "filter {{type}}",
"refId": "A"
},
{
"expr": "ours_rp_rtr_slurm_assertions",
"legendFormat": "assert {{type}}",
"refId": "B"
}
]
},
{
"id": 8,
"title": "Cache Ready / Delta Window",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 8,
"x": 0,
"y": 12
},
"fieldConfig": {
"defaults": {
"unit": "short",
"min": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_rtr_cache_ready",
"legendFormat": "ready",
"refId": "A"
},
{
"expr": "ours_rp_rtr_cache_delta_window_length",
"legendFormat": "v{{version}} length",
"refId": "B"
}
]
},
{
"id": 9,
"title": "Cache Snapshot Items",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 8,
"x": 8,
"y": 12
},
"fieldConfig": {
"defaults": {
"unit": "short",
"min": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_rtr_cache_snapshot_items",
"legendFormat": "v{{version}} {{type}}",
"refId": "A"
}
]
},
{
"id": 10,
"title": "Latest Delta Items",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 8,
"x": 16,
"y": 12
},
"fieldConfig": {
"defaults": {
"unit": "short",
"min": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_rtr_cache_delta_items",
"legendFormat": "v{{version}} {{direction}} {{type}}",
"refId": "A"
}
]
},
{
"id": 11,
"title": "Connections",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 8,
"x": 0,
"y": 20
},
"fieldConfig": {
"defaults": {
"unit": "short",
"min": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_rtr_active_connections",
"legendFormat": "active",
"refId": "A"
},
{
"expr": "ours_rp_rtr_connections",
"legendFormat": "{{transport}}",
"refId": "B"
}
]
},
{
"id": 12,
"title": "Connection Utilization / Max",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 8,
"x": 8,
"y": 20
},
"fieldConfig": {
"defaults": {
"min": 0
},
"overrides": [
{
"matcher": {
"id": "byRegexp",
"options": ".*utilization.*"
},
"properties": [
{
"id": "unit",
"value": "percentunit"
}
]
},
{
"matcher": {
"id": "byRegexp",
"options": ".*max.*"
},
"properties": [
{
"id": "unit",
"value": "short"
}
]
}
]
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_rtr_connection_utilization",
"legendFormat": "utilization",
"refId": "A"
},
{
"expr": "ours_rp_rtr_max_connections",
"legendFormat": "max",
"refId": "B"
}
]
},
{
"id": 13,
"title": "Report Age",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 8,
"x": 16,
"y": 20
},
"fieldConfig": {
"defaults": {
"unit": "s",
"min": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_rtr_source_report_age_seconds",
"legendFormat": "source",
"refId": "A"
},
{
"expr": "ours_rp_rtr_runtime_report_age_seconds",
"legendFormat": "runtime",
"refId": "B"
},
{
"expr": "ours_rp_rtr_clients_report_age_seconds",
"legendFormat": "clients",
"refId": "C"
}
]
}
],
"refresh": "10s",
"schemaVersion": 40,
"tags": [
"rpki",
"inter-rp",
"routinator"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timezone": "browser",
"title": "RTR Service Overview",
"uid": "ours-rp-rtr-overview",
"version": 1
}

View File

@@ -0,0 +1,875 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"unit": "none"
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 0,
"w": 6,
"h": 4
},
"id": 1,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "max(ours_rp_cir_trust_anchors and on (exported_instance) topk(1, ours_rp_run_sequence))",
"legendFormat": "RIRs",
"refId": "A"
}
],
"title": "Current Run RIRs",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "s"
},
"overrides": []
},
"gridPos": {
"x": 6,
"y": 0,
"w": 6,
"h": 4
},
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "max(ours_rp_run_duration_seconds and on (exported_instance) topk(1, ours_rp_run_sequence))",
"legendFormat": "wall",
"refId": "A"
}
],
"title": "Latest Wall Time",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"x": 12,
"y": 0,
"w": 6,
"h": 4
},
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "max(ours_rp_run_max_rss_bytes and on (exported_instance) topk(1, ours_rp_run_sequence))",
"legendFormat": "rss",
"refId": "A"
}
],
"title": "Latest Max RSS",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"unit": "none"
},
"overrides": []
},
"gridPos": {
"x": 18,
"y": 0,
"w": 6,
"h": 4
},
"id": 4,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "max(ours_rp_publication_points and on (exported_instance) topk(1, ours_rp_run_sequence))",
"legendFormat": "publication points",
"refId": "A"
}
],
"title": "Publication Points",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"unit": "none"
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 4,
"w": 6,
"h": 4
},
"id": 9,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "max(ours_rp_run_sequence)",
"legendFormat": "seq",
"refId": "A"
}
],
"title": "Latest Run Sequence",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"decimals": 2,
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": null
},
{
"color": "orange",
"value": 90
},
{
"color": "green",
"value": 98
}
]
}
},
"overrides": []
},
"gridPos": {
"x": 6,
"y": 4,
"w": 6,
"h": 4
},
"id": 10,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "max((100 * sum by (exported_instance) (ours_rp_repo_terminal_state_count{terminal_state=\"publication_point_cache\"}) / sum by (exported_instance) (ours_rp_publication_points)) and on (exported_instance) topk(1, ours_rp_run_sequence))",
"legendFormat": "PP cache hit ratio",
"refId": "A"
}
],
"title": "Latest PP Cache Hit Ratio",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0
},
"overrides": []
},
"gridPos": {
"x": 12,
"y": 4,
"w": 6,
"h": 4
},
"id": 11,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "max(ours_rp_vrps{kind=\"total\"} and on (exported_instance) topk(1, ours_rp_run_sequence))",
"legendFormat": "VRPs raw",
"refId": "A"
}
],
"title": "VRPs",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0
},
"overrides": []
},
"gridPos": {
"x": 18,
"y": 4,
"w": 6,
"h": 4
},
"id": 12,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "max(ours_rp_vaps and on (exported_instance) topk(1, ours_rp_run_sequence))",
"legendFormat": "VAPs",
"refId": "A"
}
],
"title": "VAPs",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "s"
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 8,
"w": 12,
"h": 8
},
"id": 5,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_run_duration_seconds",
"legendFormat": "wall",
"refId": "A"
},
{
"expr": "ours_rp_stage_duration_seconds{stage=\"validation\"}",
"legendFormat": "validation",
"refId": "B"
}
],
"title": "Run / Validation Duration",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"unit": "none"
},
"overrides": []
},
"gridPos": {
"x": 12,
"y": 8,
"w": 12,
"h": 8
},
"id": 6,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_vrps{kind=\"total\"}",
"legendFormat": "VRPs raw",
"refId": "A"
},
{
"expr": "ours_rp_vrps{kind=\"unique\"}",
"legendFormat": "VRPs unique",
"refId": "D"
},
{
"expr": "ours_rp_vaps",
"legendFormat": "VAPs",
"refId": "B"
},
{
"expr": "ours_rp_cir_objects",
"legendFormat": "CIR objects",
"refId": "C"
}
],
"title": "Output and Input Sizes",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"unit": "none"
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 16,
"w": 12,
"h": 8
},
"id": 8,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_large_publication_points",
"legendFormat": "> {{object_count_gt}} objects",
"refId": "A"
}
],
"title": "Large Publication Points by Object Count",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "s"
},
"overrides": []
},
"gridPos": {
"x": 12,
"y": 16,
"w": 12,
"h": 8
},
"id": 13,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_run_stage_duration_seconds{stage=\"validation\"}",
"legendFormat": "validation",
"refId": "A"
},
{
"expr": "ours_rp_run_stage_duration_seconds{stage=\"report_write\"}",
"legendFormat": "report write",
"refId": "E"
},
{
"expr": "ours_rp_run_stage_duration_seconds{stage=\"ccr_write\"}",
"legendFormat": "ccr write",
"refId": "F"
},
{
"expr": "ours_rp_run_stage_duration_seconds{stage=\"cir_write\"}",
"legendFormat": "cir write",
"refId": "G"
}
],
"title": "Output Stage Durations",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "bytes",
"decimals": 2
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 24,
"w": 12,
"h": 8
},
"id": 14,
"options": {
"legend": {
"calcs": [
"lastNotNull",
"max"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_run_max_rss_bytes",
"legendFormat": "Max RSS",
"refId": "A"
}
],
"title": "Max RSS Over Time",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "percent",
"decimals": 2,
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": null
},
{
"color": "orange",
"value": 90
},
{
"color": "green",
"value": 98
}
]
}
},
"overrides": []
},
"gridPos": {
"x": 12,
"y": 24,
"w": 12,
"h": 8
},
"id": 17,
"options": {
"legend": {
"calcs": [
"lastNotNull",
"min",
"max"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "100 * sum by (job, instance, exported_instance) (ours_rp_repo_terminal_state_count{terminal_state=\"publication_point_cache\"}) / sum by (job, instance, exported_instance) (ours_rp_publication_points)",
"legendFormat": "PP cache hit ratio",
"refId": "A"
}
],
"title": "PP Cache Hit Ratio",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "bytes",
"decimals": 2
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 32,
"w": 24,
"h": 8
},
"id": 15,
"options": {
"legend": {
"calcs": [
"lastNotNull",
"max"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_state_db_size_bytes",
"legendFormat": "{{db}}",
"refId": "A"
}
],
"title": "State DB Size Over Time",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 40,
"w": 24,
"h": 8
},
"id": 16,
"options": {
"legend": {
"calcs": [
"lastNotNull",
"max"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_state_db_files",
"legendFormat": "{{db}}",
"refId": "A"
}
],
"title": "State DB File Count Over Time",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0,
"min": 0
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 48,
"w": 24,
"h": 8
},
"id": 18,
"options": {
"legend": {
"calcs": [
"lastNotNull",
"max"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "sum by (job, instance, exported_instance) (ours_rp_repo_terminal_state_count{terminal_state=\"fresh\"})",
"legendFormat": "fresh pp",
"refId": "A"
},
{
"expr": "sum by (job, instance, exported_instance) (ours_rp_cir_objects_by_source_type{exported_source=\"fresh\",object_type=\"roa\"})",
"legendFormat": "fresh roa",
"refId": "B"
},
{
"expr": "sum by (job, instance, exported_instance) (ours_rp_cir_objects_by_source_type{exported_source=\"fresh\",object_type=\"manifest\"})",
"legendFormat": "fresh mft",
"refId": "C"
},
{
"expr": "sum by (job, instance, exported_instance) (ours_rp_cir_objects_by_source_type{exported_source=\"fresh\",object_type=\"certificate\"})",
"legendFormat": "fresh crt",
"refId": "D"
},
{
"expr": "sum by (job, instance, exported_instance) (ours_rp_cir_objects_by_source_type{exported_source=\"fresh\",object_type=\"crl\"})",
"legendFormat": "fresh crl",
"refId": "E"
}
],
"title": "Fresh PP / Object Counts by Run",
"type": "timeseries"
}
],
"refresh": "5s",
"schemaVersion": 40,
"tags": [
"ours-rp",
"rpki",
"soak"
],
"templating": {
"list": []
},
"time": {
"from": "now-30m",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "Ours RP Soak Overview",
"uid": "ours-rp-soak-overview",
"version": 4,
"weekStart": ""
}

View File

@@ -0,0 +1,12 @@
apiVersion: 1
providers:
- name: ours-rp-docker-installer
orgId: 1
folder: Ours RP Docker Installer
type: file
disableDeletion: false
updateIntervalSeconds: 10
allowUiUpdates: true
options:
path: /var/lib/grafana/dashboards

View File

@@ -0,0 +1,10 @@
apiVersion: 1
datasources:
- name: Prometheus
uid: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true

View File

@@ -0,0 +1,13 @@
global:
scrape_interval: 5s
evaluation_interval: 5s
scrape_configs:
- job_name: ours-rp-artifact-metrics
metrics_path: /metrics
static_configs:
- targets:
- artifact-metrics:9556
labels:
rp: ours-rp
source: docker-installer-artifact-sidecar

View File

@@ -0,0 +1,194 @@
# ours RP Docker Installer Guide (`__PACKAGE_ARCH__` / `__PACKAGE_PLATFORM__`)
## Goal
This package deploys ours RP on Linux servers whose host architecture matches the packaged installer metadata, using Docker Compose and continuous all-five RIR validation.
The package includes four architecture-matched images: ours RP runtime, artifact metrics, Prometheus, and Grafana. Deployment does not need to pull application images on the target host. Runtime state, run artifacts, logs, Prometheus data and Grafana data are persisted through host bind mounts.
Every installer build rebuilds the ours RP runtime and artifact metrics images. The deliverable name is `ours-rp-installer-{arch}-{git8}-{YYYYMMDDTHHMMSSZ}.tar.gz`, and both ours RP image tags use the current source commit. `PACKAGE-MANIFEST.env` records the full commit, UTC build time, dirty state, and image archive hashes; `./scripts/status.sh --brief` displays that provenance after installation.
By default the host architecture must match `PACKAGE_ARCH` from `PACKAGE-MANIFEST.env`. If you intentionally want to run this package on a different host architecture through QEMU/binfmt, set:
```bash
ALLOW_CROSS_ARCH=1
```
## Compose Template Source
`compose/` is the single production Docker Compose template source. The Docker installer build script copies this directory directly. Do not maintain a second copy of the Compose, Prometheus, or Grafana dashboard files elsewhere.
The legacy direct Compose deployment entry point has been removed. For either amd64 or arm64, build the matching installer package with `scripts/docker/build_docker_installer_package.sh`, then use `scripts/install.sh`, `scripts/start.sh`, and `scripts/status.sh` as described here.
## Quick Start
```bash
tar -xzf ours-rp-installer-__PACKAGE_ARCH__-*.tar.gz
cd ours-rp-installer-__PACKAGE_ARCH__-*
./scripts/install.sh
cp .env.example .env # install.sh creates .env automatically if missing
vim .env
./scripts/start.sh
./scripts/status.sh
```
Defaults:
- `PACKAGE_ARCH=__PACKAGE_ARCH__`
- `PACKAGE_PLATFORM=__PACKAGE_PLATFORM__`
- `RIRS=afrinic,apnic,arin,lacnic,ripe`
- `MAX_RUNS=-1`
- `INTERVAL_SECS=600`
- `TAL_INPUT_MODE=file-live-ta`
- `RESOURCE_VALIDATION_MODE=validation-update-03`
- `LIVE_TA_REFRESH_BEFORE_SNAPSHOT=1`
- `PERIODIC_SNAPSHOT_RESET=0`
- `PERIODIC_SNAPSHOT_MAX_DELTAS=100`
- `HOST_DATA_DIR=__HOST_DATA_DIR__`
- `SOAK_RESTART_POLICY=unless-stopped`
- `RPKI_IMAGE=__RUNTIME_IMAGE__`
- `METRICS_IMAGE=__METRICS_IMAGE__`
- `METRICS_PLATFORM=__PACKAGE_PLATFORM__`
- `MONITOR_PLATFORM=__PACKAGE_PLATFORM__`
- `ALLOW_CROSS_ARCH=0`
- `RTR_REPORT_DIR=__HOST_DATA_DIR__/empty-rtr-report`, which mounts an empty fallback directory by default; after deploying a separate RTR service, register its same-host report directory through `scripts/register_rtr_monitor.sh`
## First Start Semantics
If there is no successful run under `HOST_DATA_DIR/runs`, `start.sh` starts the core `ours-rp-soak` service first and waits for the first snapshot to succeed before starting metrics, Prometheus and Grafana.
Container/image mapping:
- `ours-rp-soak` uses `RPKI_IMAGE`
- `artifact-metrics` uses `METRICS_IMAGE`
- `prometheus` / `grafana` use their monitor images
The first snapshot refreshes live TA certificates before starting the RP process.
## Resource Validation Mode
New knob:
```bash
RESOURCE_VALIDATION_MODE=validation-update-03
```
Supported values:
- `validation-update-03`: default behavior, using the current validation-update draft semantics;
- `rfc6487`: switch back to the original RFC 6487 resource containment behavior.
The installer / soak runner passes `--resource-validation-mode` to every `rpki` child start.
When an upgraded deployment reuses an older `.env` that does not contain this key, the installer appends the new default `validation-update-03` automatically.
## Architecture Guardrails
The key scripts read:
- `PACKAGE_ARCH` / `PACKAGE_PLATFORM` from `.env`
- `PACKAGE-MANIFEST.env`
- the current host `uname -m`
Default behavior:
1. matching host and package architectures: run natively;
2. mismatched host and package architectures: fail with an explicit error;
3. only when `ALLOW_CROSS_ARCH=1` is set do the scripts attempt to enable matching `binfmt/qemu`.
That means an `arm64` package on an `x86_64` host does not silently switch to emulation by default.
## Ports
Default ports:
- metrics: `http://<host>:9556/metrics`
- Prometheus: `http://<host>:9090`
- Grafana: `http://<host>:3000`
Grafana credentials come from `.env`:
```bash
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=admin
```
Change the password and restrict public access for production deployments.
## Optional RTR report metrics input
If a separately deployed RTR service on the same host continuously writes `rtr-source-*`, `rtr-runtime-*`, and `rtr-clients-*` JSON reports, run:
```bash
./scripts/register_rtr_monitor.sh --report-dir /root/rpki/report
```
The command accepts only an absolute path on the same host, validates the candidate Compose mount, and atomically updates `.env`. It recreates only `artifact-metrics`, Prometheus, and Grafana; it does not restart or recreate `ours-rp-soak`. The `artifact-metrics` container mounts the directory read-only and reads it through `RPKI_METRICS_RTR_REPORT_DIR`, producing `ours_rp_rtr_*` metrics. By default, `RTR_REPORT_DIR` points at an empty fallback directory under the installer data root, so the metrics container also starts cleanly without an RTR service.
## Data Directory
Default host directory:
```text
__HOST_DATA_DIR__/
state/
runs/
logs/
tmp/
prometheus/
grafana/
```
Each `runs/run_XXXX/` directory contains `report.json`, `result.ccr`, `input.cir`, `vrps.csv`, `vaps.csv`, `stage-timing.json`, logs and metadata.
## Periodic Snapshot Reset
New knobs:
```bash
PERIODIC_SNAPSHOT_RESET=0
PERIODIC_SNAPSHOT_MAX_DELTAS=100
```
Semantics:
- disabled by default, keeping previous behavior unchanged;
- when enabled, one successful snapshot is followed by at most `N` successful delta runs;
- after the threshold is reached, the next run is forced to snapshot;
- before that forced snapshot, only the active `state/db` is reset, while `runs/`, `logs/`, `state/rsync-mirror`, `.env`, and Prometheus/Grafana data are preserved;
- the counter is persisted independently in `HOST_DATA_DIR/state/run-lifecycle-state.json`, so it does not depend on retained `runs/` history;
- if that lifecycle file is corrupt, it is backed up as `run-lifecycle-state.json.corrupt.<timestamp>.<pid>` before best-effort bootstrap from retained runs;
- after a successful forced snapshot, the old DB staging is deleted so disk usage does not keep growing elsewhere.
Check the latest `run-meta.json` for:
- `sync_mode`
- `snapshot_reason`
- `periodic_snapshot_delta_count`
- `periodic_snapshot_forced`
- `reset_db_cleanup_status`
## Common Commands
```bash
./scripts/status.sh
./scripts/logs.sh ours-rp-soak --tail 200
./scripts/restart.sh
./scripts/stop.sh
./scripts/cleanup.sh --keep-runs 100 --execute
./scripts/uninstall.sh
```
For finite acceptance tests, for example `MAX_RUNS=3`, also set:
```bash
SOAK_RESTART_POLICY=no
```
Otherwise Compose `unless-stopped` will restart the container after it exits successfully.
`uninstall.sh` keeps data by default. Use the following only when you really want to delete `HOST_DATA_DIR`:
```bash
./scripts/uninstall.sh --purge-data
```

View File

@@ -0,0 +1,196 @@
# ours RP Docker 安装包使用说明(`__PACKAGE_ARCH__` / `__PACKAGE_PLATFORM__`)
## 目标
本安装包用于在目标架构与包元数据匹配的 Linux 服务器上,通过 Docker Compose 部署 ours RP,并持续运行 all5 RIR 同步验证任务。
安装包内置与包架构一致的四类镜像:ours RP runtime、artifact metrics、Prometheus、Grafana。部署时不需要现场拉取应用镜像。运行产物、状态数据库、日志、Prometheus 和 Grafana 数据均通过宿主机目录挂载保存。
每次构建安装包都会重新构建 ours RP runtime 与 artifact metrics 镜像。交付包命名为 `ours-rp-installer-{arch}-{git8}-{YYYYMMDDTHHMMSSZ}.tar.gz`,两类 ours RP 镜像分别使用当前源码 commit 的 tag。完整 commit、UTC 构建时间、dirty 状态和镜像归档 hash 保存在 `PACKAGE-MANIFEST.env`,可通过 `./scripts/status.sh --brief` 查看。
默认情况下,宿主架构必须与 `PACKAGE-MANIFEST.env` 中的 `PACKAGE_ARCH` 匹配;若确需在异构主机上通过 QEMU/binfmt 运行,必须显式设置:
```bash
ALLOW_CROSS_ARCH=1
```
## Compose 模板来源
`compose/` 是唯一的生产 Docker Compose 模板来源,Docker 安装包构建脚本会直接复制该目录。不要在其他目录维护第二份 compose、Prometheus 或 Grafana dashboard 文件。
旧的直接 Compose 部署入口已经移除。需要部署 amd64 或 arm64 时,统一先用 `scripts/docker/build_docker_installer_package.sh` 构建对应架构安装包,再按本说明执行 `scripts/install.sh`、`scripts/start.sh` 和 `scripts/status.sh`。
## 快速开始
```bash
tar -xzf ours-rp-installer-__PACKAGE_ARCH__-*.tar.gz
cd ours-rp-installer-__PACKAGE_ARCH__-*
./scripts/install.sh
cp .env.example .env # 如 install.sh 已自动创建,可直接编辑现有 .env
vim .env
./scripts/start.sh
./scripts/status.sh
```
默认配置:
- `PACKAGE_ARCH=__PACKAGE_ARCH__`
- `PACKAGE_PLATFORM=__PACKAGE_PLATFORM__`
- `RIRS=afrinic,apnic,arin,lacnic,ripe`
- `MAX_RUNS=-1`
- `INTERVAL_SECS=600`
- `TAL_INPUT_MODE=file-live-ta`
- `RESOURCE_VALIDATION_MODE=validation-update-03`
- `LIVE_TA_REFRESH_BEFORE_SNAPSHOT=1`
- `PERIODIC_SNAPSHOT_RESET=0`
- `PERIODIC_SNAPSHOT_MAX_DELTAS=100`
- `HOST_DATA_DIR=__HOST_DATA_DIR__`
- `SOAK_RESTART_POLICY=unless-stopped`
- `RPKI_IMAGE=__RUNTIME_IMAGE__`
- `METRICS_IMAGE=__METRICS_IMAGE__`
- `METRICS_PLATFORM=__PACKAGE_PLATFORM__`
- `MONITOR_PLATFORM=__PACKAGE_PLATFORM__`
- `ALLOW_CROSS_ARCH=0`
- `RTR_REPORT_DIR=__HOST_DATA_DIR__/empty-rtr-report`,默认挂载空目录;独立 RTR 服务部署完成后,使用 `scripts/register_rtr_monitor.sh` 注册同机 report 目录
## 首次启动语义
如果 `HOST_DATA_DIR/runs` 下没有成功 run,`start.sh` 会先启动核心 `ours-rp-soak`,等待第一轮 snapshot 成功后再启动 metrics、Prometheus 和 Grafana。
容器镜像分工:
- `ours-rp-soak` 使用 `RPKI_IMAGE`
- `artifact-metrics` 使用 `METRICS_IMAGE`
- `prometheus` / `grafana` 使用各自 monitor image
第一轮 snapshot 会先拉取 live TA,避免 clean state 使用旧 fixture TA。
## 资源验证模式
新增配置:
```bash
RESOURCE_VALIDATION_MODE=validation-update-03
```
可选值:
- `validation-update-03`:默认,按当前 validation update draft 语义运行;
- `rfc6487`:切换为 RFC 6487 原始资源包含判定语义。
installer / soak runner 每次启动 `rpki` 子进程时都会显式传入 `--resource-validation-mode`。
如果升级时复用旧 `.env` 且缺少该变量,installer 会自动补入默认值 `validation-update-03`,不会破坏旧 `.env` 复用流程。
## 架构检查
关键脚本会读取:
- `.env` 中的 `PACKAGE_ARCH` / `PACKAGE_PLATFORM`
- `PACKAGE-MANIFEST.env`
- 当前宿主 `uname -m`
默认行为:
1. 宿主与包架构匹配:直接运行;
2. 宿主与包架构不匹配:明确报错并停止;
3. 仅当 `ALLOW_CROSS_ARCH=1` 时,脚本才会尝试启用对应架构的 `binfmt/qemu`。
因此在 `x86_64` 主机上运行 `arm64` 包,不会默认静默进入模拟执行。
## 访问端口
默认端口:
- metrics: `http://<host>:9556/metrics`
- Prometheus: `http://<host>:9090`
- Grafana: `http://<host>:3000`
Grafana 默认账号密码来自 `.env`:
```bash
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=admin
```
生产部署时应修改密码并限制外部访问。
## 可选 RTR report 监控接入
如果同一台机器上已经有独立部署的 RTR 服务,并且它持续输出 `rtr-source-*`、`rtr-runtime-*`、`rtr-clients-*` JSON report,执行:
```bash
./scripts/register_rtr_monitor.sh --report-dir /root/rpki/report
```
该命令只接受本机绝对路径,会先校验候选 Compose 挂载,再原子更新 `.env`。随后只重建 `artifact-metrics`、Prometheus 和 Grafana,不重启或重建 `ours-rp-soak`。`artifact-metrics` 以只读方式挂载该目录,并通过 `RPKI_METRICS_RTR_REPORT_DIR` 读取 report,输出 `ours_rp_rtr_*` 指标。默认 `RTR_REPORT_DIR` 指向安装包数据目录下的空 fallback 目录,因此未接入 RTR 服务时 metrics 容器也能稳定启动。
## 数据目录
默认宿主机目录:
```text
__HOST_DATA_DIR__/
state/
runs/
logs/
tmp/
prometheus/
grafana/
```
`runs/run_XXXX/` 中包含每轮 `report.json`、`result.ccr`、`input.cir`、`vrps.csv`、`vaps.csv`、`stage-timing.json`、日志和元数据。
## 定期 snapshot reset
新增配置:
```bash
PERIODIC_SNAPSHOT_RESET=0
PERIODIC_SNAPSHOT_MAX_DELTAS=100
```
语义:
- 默认关闭,行为与旧版本一致;
- 开启后,一次成功 snapshot 后最多连续执行 `N` 个成功 delta;
- 达到阈值后,下一轮强制跑 snapshot;
- 强制 snapshot 前只重置 active `state/db`,保留 `runs/`、`logs/`、`state/rsync-mirror`、`.env`、Prometheus/Grafana 数据;
- 周期计数保存在独立 lifecycle 文件 `HOST_DATA_DIR/state/run-lifecycle-state.json`,不依赖 `runs/` 保留窗口;
- lifecycle 文件损坏时会先备份为 `run-lifecycle-state.json.corrupt.<timestamp>.<pid>`,再从当前保留 run 尽力 bootstrap;
- 强制 snapshot 成功后旧 DB staging 会被删除,避免磁盘只是换目录继续增长。
可通过最新 `run-meta.json` 中的以下字段确认:
- `sync_mode`
- `snapshot_reason`
- `periodic_snapshot_delta_count`
- `periodic_snapshot_forced`
- `reset_db_cleanup_status`
## 常用命令
```bash
./scripts/status.sh
./scripts/logs.sh ours-rp-soak --tail 200
./scripts/restart.sh
./scripts/stop.sh
./scripts/cleanup.sh --keep-runs 100 --execute
./scripts/uninstall.sh
```
如果做有限轮次验收,例如 `MAX_RUNS=3`,建议同时设置:
```bash
SOAK_RESTART_POLICY=no
```
否则 Compose 的 `unless-stopped` 策略会在容器正常退出后再次拉起下一轮。
`uninstall.sh` 默认不删除数据。只有显式执行:
```bash
./scripts/uninstall.sh --purge-data
```
才会删除 `HOST_DATA_DIR`。

View File

@@ -0,0 +1,137 @@
# Operations Guide (`__PACKAGE_ARCH__`)
## Install
```bash
./scripts/install.sh
```
The installer is idempotent:
- existing `.env` is kept;
- existing Docker/Compose installation is reused;
- repeated loading of packaged runtime, metrics, Prometheus and Grafana images for the package architecture is safe;
- existing data directory is reused;
- matching host/package architecture is required by default.
## Start
```bash
./scripts/start.sh
```
Start without waiting for the first snapshot:
```bash
./scripts/start.sh --no-wait-first-run
```
If you intentionally want to run this package on a different host architecture through QEMU/binfmt, first set:
```bash
ALLOW_CROSS_ARCH=1
```
Then rerun `./scripts/install.sh` or `./scripts/self-check.sh`, and only then will the scripts attempt to enable the matching binfmt handler.
## Stop and Restart
```bash
./scripts/stop.sh
./scripts/restart.sh
```
## Status Checks
```bash
./scripts/status.sh
./scripts/self-check.sh
```
Important checks:
- Docker/Compose availability;
- runtime, metrics, Prometheus and Grafana images exist;
- `HOST_DATA_DIR` is writable;
- Compose config is valid;
- latest run status;
- metrics, Prometheus and Grafana endpoints;
- whether `host_arch`, `package_arch`, and `arch_mode` match expectations.
`status.sh` also prints:
- `package_arch`
- `package_platform`
- `runtime_image`
- `metrics_image`
- `allow_cross_arch`
- `periodic_snapshot_reset`
- `periodic_snapshot_max_deltas`
- `rtr_report_dir`
- `rtr_report_container_dir`
To verify RTR report ingestion:
```bash
./scripts/register_rtr_monitor.sh --report-dir /root/rpki/report --dry-run
./scripts/register_rtr_monitor.sh --report-dir /root/rpki/report
./scripts/status.sh
curl -s "http://127.0.0.1:${METRICS_PORT:-9556}/metrics" | grep '^ours_rp_rtr_' | head
```
Registration accepts only an absolute directory on the same host. The command retains a timestamped `.env` backup and updates only the metrics, Prometheus, and Grafana sidecars; the `ours-rp-soak` container ID must remain unchanged.
## Upgrade
Extract the new package into a new directory and explicitly reuse the existing `.env` through the upgrade script:
```bash
./scripts/upgrade.sh --reuse-env-from /path/to/old-installer/.env
```
If the new package directory already has a `.env`, the upgrade script keeps it.
If an older `.env` does not contain `METRICS_IMAGE` or `METRICS_PLATFORM`, the install/upgrade flow backfills package-matched defaults automatically.
Upgrade does not delete:
- `runs/`
- `logs/`
- `state/rsync-mirror`
- runtime configuration referenced by `.env`
- Prometheus / Grafana data
To validate periodic forced snapshot behavior, temporarily set:
```bash
PERIODIC_SNAPSHOT_RESET=1
PERIODIC_SNAPSHOT_MAX_DELTAS=2
```
Then confirm the latest `run-meta.json` contains:
```bash
snapshot_reason=periodic_snapshot_delta_limit
```
And inspect the independent lifecycle state:
```bash
jq '{last_run,last_success_snapshot,successful_deltas_since_snapshot,state_health}' \
"${HOST_DATA_DIR}/state/run-lifecycle-state.json"
```
After validation, restore:
```bash
PERIODIC_SNAPSHOT_MAX_DELTAS=100
```
## Cleanup
```bash
./scripts/cleanup.sh --keep-runs 100
./scripts/cleanup.sh --keep-runs 100 --execute
```
Cleanup is dry-run by default. Add `--execute` after reviewing the output.

View File

@@ -0,0 +1,137 @@
# 运维手册(`__PACKAGE_ARCH__`)
## 安装
```bash
./scripts/install.sh
```
安装脚本是幂等的:
- 已有 `.env` 不覆盖;
- 已安装 Docker/Compose 则跳过;
- 包内 runtime、metrics、Prometheus、Grafana 对应架构镜像重复加载是安全的;
- 数据目录已存在则复用;
- 默认要求宿主与包架构匹配。
## 启动
```bash
./scripts/start.sh
```
如需后台启动后不等待首轮 snapshot:
```bash
./scripts/start.sh --no-wait-first-run
```
如需显式允许异构主机通过 QEMU/binfmt 跑包,先在 `.env` 中设置:
```bash
ALLOW_CROSS_ARCH=1
```
然后重新执行 `./scripts/install.sh` 或 `./scripts/self-check.sh`,脚本才会尝试启用对应 binfmt。
## 停止和重启
```bash
./scripts/stop.sh
./scripts/restart.sh
```
## 状态检查
```bash
./scripts/status.sh
./scripts/self-check.sh
```
重点检查项:
- Docker/Compose 可用;
- runtime、metrics、Prometheus、Grafana 镜像存在;
- `HOST_DATA_DIR` 可写;
- Compose 配置合法;
- 最新 run 状态;
- metrics、Prometheus、Grafana endpoint;
- `host_arch`、`package_arch`、`arch_mode` 是否符合预期。
`status.sh` 还会显示:
- `package_arch`
- `package_platform`
- `runtime_image`
- `metrics_image`
- `allow_cross_arch`
- `periodic_snapshot_reset`
- `periodic_snapshot_max_deltas`
- `rtr_report_dir`
- `rtr_report_container_dir`
如需确认 RTR report 已接入:
```bash
./scripts/register_rtr_monitor.sh --report-dir /root/rpki/report --dry-run
./scripts/register_rtr_monitor.sh --report-dir /root/rpki/report
./scripts/status.sh
curl -s "http://127.0.0.1:${METRICS_PORT:-9556}/metrics" | grep '^ours_rp_rtr_' | head
```
注册只支持同机的绝对目录。注册命令会保留一份带时间戳的 `.env` 备份,并且只更新 metrics、Prometheus、Grafana 三个旁路容器;`ours-rp-soak` 的容器 ID 应保持不变。
## 升级
把新安装包解压到新目录后,推荐通过升级脚本显式复用旧 `.env`:
```bash
./scripts/upgrade.sh --reuse-env-from /path/to/old-installer/.env
```
如果新目录已经存在 `.env`,升级脚本会保留它,不覆盖。
如果旧 `.env` 缺失 `METRICS_IMAGE` 或 `METRICS_PLATFORM`,升级/安装链路会按当前包架构自动补齐默认值。
升级不会删除以下数据:
- `runs/`
- `logs/`
- `state/rsync-mirror`
- `.env` 对应的运行配置
- Prometheus / Grafana 数据
验证定期 forced snapshot 时,可临时设置:
```bash
PERIODIC_SNAPSHOT_RESET=1
PERIODIC_SNAPSHOT_MAX_DELTAS=2
```
然后检查最新 `run-meta.json` 应出现:
```bash
snapshot_reason=periodic_snapshot_delta_limit
```
并检查独立 lifecycle 状态:
```bash
jq '{last_run,last_success_snapshot,successful_deltas_since_snapshot,state_health}' \
"${HOST_DATA_DIR}/state/run-lifecycle-state.json"
```
验证完成后恢复:
```bash
PERIODIC_SNAPSHOT_MAX_DELTAS=100
```
## 清理
```bash
./scripts/cleanup.sh --keep-runs 100
./scripts/cleanup.sh --keep-runs 100 --execute
```
默认 dry-run,确认后加 `--execute`。

View File

@@ -0,0 +1,142 @@
# Troubleshooting (`__PACKAGE_ARCH__` / `__PACKAGE_PLATFORM__`)
## Docker or Compose Is Unavailable
Run:
```bash
docker version
docker compose version
```
If missing, run:
```bash
./scripts/install.sh
```
## Package Architecture Mismatch
If `status.sh`, `install.sh`, or `self-check.sh` reports:
```text
package architecture mismatch
```
the host `uname -m` does not match the packaged `PACKAGE_ARCH=__PACKAGE_ARCH__`. The default behavior is to fail clearly rather than silently switching to emulation.
Only if you intentionally accept QEMU/binfmt cross-architecture execution should you set:
```bash
ALLOW_CROSS_ARCH=1
```
Then rerun install or self-check.
## `__PACKAGE_ARCH__` Image Cannot Run
Running `__PACKAGE_PLATFORM__` images on a different host architecture requires binfmt/qemu. The installer only attempts this automatically when `ALLOW_CROSS_ARCH=1`; you can also enable it manually:
```bash
docker run --rm --privileged tonistiigi/binfmt --install __PACKAGE_ARCH__
docker run --rm --platform __PACKAGE_PLATFORM__ debian:bookworm-slim uname -m
```
For `arm64` packages the expected output is `aarch64`; for `amd64` packages the expected output is `x86_64`.
To confirm the metrics image is also loaded correctly:
```bash
docker image inspect "$(grep '^METRICS_IMAGE=' .env | cut -d= -f2-)"
docker run --rm --platform __PACKAGE_PLATFORM__ \
"$(grep '^METRICS_IMAGE=' .env | cut -d= -f2-)" \
/opt/ours-rp/bin/rpki_artifact_metrics --help
```
## RTR report metrics are empty
First check registration and the report directory:
```bash
./scripts/status.sh
./scripts/register_rtr_monitor.sh --report-dir /root/rpki/report --dry-run
```
Check the metrics container mount:
```bash
docker inspect "$(grep '^COMPOSE_PROJECT_NAME=' .env | cut -d= -f2-)-artifact-metrics" \
| jq '.[0].Mounts[] | select(.Destination=="/var/lib/ours-rp/rtr-report")'
```
## First Snapshot Times Out
All-five snapshot can be slow, especially under QEMU. Increase timeout:
```bash
./scripts/start.sh --timeout-secs 14400
```
## Output Counts Are Too Low
Check:
```bash
grep LIVE_TA_REFRESH_BEFORE_SNAPSHOT .env
ls -l __HOST_DATA_DIR__/state/live-ta
tail -100 __HOST_DATA_DIR__/logs/live-ta-refresh-*.log
```
In `file-live-ta` mode, snapshot should wait until live TA refresh succeeds.
## Grafana Login Fails
Check `.env`:
```bash
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=admin
```
If Grafana has already started, changing `.env` may not reset the existing Grafana database. Stop services and back up/clean `${HOST_DATA_DIR}/grafana` if needed.
## A Finite Acceptance Test Starts an Extra Run
If `.env` sets a finite `MAX_RUNS=3` while `SOAK_RESTART_POLICY=unless-stopped`, Docker Compose restarts the soak container after it exits successfully.
For finite tests, set:
```bash
SOAK_RESTART_POLICY=no
```
## How to Confirm a Periodic Forced Snapshot
Check the latest run metadata:
```bash
latest="$(find ${HOST_DATA_DIR}/runs -maxdepth 1 -type d -name 'run_*' | sort | tail -1)"
jq '{run_id,sync_mode,snapshot_reason,periodic_snapshot_delta_count,periodic_snapshot_forced,reset_db_cleanup_status}' "$latest/run-meta.json"
```
For a threshold-triggered reset you should see:
- `sync_mode: "snapshot"`
- `snapshot_reason: "periodic_snapshot_delta_limit"`
- `periodic_snapshot_forced: true`
To confirm delta counting still advances after retained runs are pruned, also inspect:
```bash
jq '{last_success_snapshot,successful_deltas_since_snapshot,recent_runs_count:(.recent_runs|length)}' \
"${HOST_DATA_DIR}/state/run-lifecycle-state.json"
```
## Lifecycle State File Is Corrupt
The script backs up the corrupt file before best-effort bootstrap from retained runs:
```bash
ls -1 "${HOST_DATA_DIR}/state"/run-lifecycle-state.json.corrupt.*
jq . "${HOST_DATA_DIR}/state/run-lifecycle-state.json"
```

View File

@@ -0,0 +1,142 @@
# 故障排查(`__PACKAGE_ARCH__` / `__PACKAGE_PLATFORM__`)
## Docker 或 Compose 不可用
执行:
```bash
docker version
docker compose version
```
如果缺失,重新执行:
```bash
./scripts/install.sh
```
## 架构不匹配报错
当 `status.sh` / `install.sh` / `self-check.sh` 报出:
```text
package architecture mismatch
```
说明宿主 `uname -m` 与包的 `PACKAGE_ARCH=__PACKAGE_ARCH__` 不一致。默认行为就是失败而不是静默模拟运行。
只有在你明确接受 QEMU/binfmt 跨架构运行时,才设置:
```bash
ALLOW_CROSS_ARCH=1
```
然后重新执行安装或自检。
## `__PACKAGE_ARCH__` 镜像无法运行
在异构主机上运行 `__PACKAGE_PLATFORM__` 镜像需要 binfmt/qemu。安装脚本只有在 `ALLOW_CROSS_ARCH=1` 时才会自动尝试启用;也可以手动执行:
```bash
docker run --rm --privileged tonistiigi/binfmt --install __PACKAGE_ARCH__
docker run --rm --platform __PACKAGE_PLATFORM__ debian:bookworm-slim uname -m
```
对于 `arm64` 包,预期输出 `aarch64`;对于 `amd64` 包,预期输出 `x86_64`。
如需进一步确认 metrics 镜像也已正确加载:
```bash
docker image inspect "$(grep '^METRICS_IMAGE=' .env | cut -d= -f2-)"
docker run --rm --platform __PACKAGE_PLATFORM__ \
"$(grep '^METRICS_IMAGE=' .env | cut -d= -f2-)" \
/opt/ours-rp/bin/rpki_artifact_metrics --help
```
## RTR report 指标为空
先检查注册状态和 report 目录:
```bash
./scripts/status.sh
./scripts/register_rtr_monitor.sh --report-dir /root/rpki/report --dry-run
```
检查 metrics 容器是否挂载该目录:
```bash
docker inspect "$(grep '^COMPOSE_PROJECT_NAME=' .env | cut -d= -f2-)-artifact-metrics" \
| jq '.[0].Mounts[] | select(.Destination=="/var/lib/ours-rp/rtr-report")'
```
## 首轮 snapshot 超时
all5 snapshot 可能很慢,尤其在 QEMU 环境。可以提高超时:
```bash
./scripts/start.sh --timeout-secs 14400
```
## 产物数量异常偏低
检查:
```bash
grep LIVE_TA_REFRESH_BEFORE_SNAPSHOT .env
ls -l __HOST_DATA_DIR__/state/live-ta
tail -100 __HOST_DATA_DIR__/logs/live-ta-refresh-*.log
```
`file-live-ta` 模式下,snapshot 应等待 live TA 成功刷新。
## Grafana 无法登录
确认 `.env` 中:
```bash
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=admin
```
如果曾经启动过 Grafana,修改 `.env` 不一定重置已有 Grafana 数据库账号。可以停止服务后按需备份并清理 `${HOST_DATA_DIR}/grafana`。
## 有限轮次验收后又多跑了一轮
如果 `.env` 中设置了 `MAX_RUNS=3` 这类有限轮次,同时 `SOAK_RESTART_POLICY=unless-stopped`,Docker Compose 会在 soak 容器正常退出后重新启动容器。
有限验收建议设置:
```bash
SOAK_RESTART_POLICY=no
```
## 如何确认触发了定期 forced snapshot
检查最新 run metadata:
```bash
latest="$(find ${HOST_DATA_DIR}/runs -maxdepth 1 -type d -name 'run_*' | sort | tail -1)"
jq '{run_id,sync_mode,snapshot_reason,periodic_snapshot_delta_count,periodic_snapshot_forced,reset_db_cleanup_status}' "$latest/run-meta.json"
```
阈值触发时应看到:
- `sync_mode: "snapshot"`
- `snapshot_reason: "periodic_snapshot_delta_limit"`
- `periodic_snapshot_forced: true`
如需确认 retain 裁剪后仍在累计 delta,可继续检查:
```bash
jq '{last_success_snapshot,successful_deltas_since_snapshot,recent_runs_count:(.recent_runs|length)}' \
"${HOST_DATA_DIR}/state/run-lifecycle-state.json"
```
## Lifecycle 状态文件损坏
脚本会先备份损坏文件,再从当前保留的 `runs/` 尽力 bootstrap:
```bash
ls -1 "${HOST_DATA_DIR}/state"/run-lifecycle-state.json.corrupt.*
jq . "${HOST_DATA_DIR}/state/run-lifecycle-state.json"
```

View File

@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=common.sh
source "$SCRIPT_DIR/common.sh"
DRY_RUN=1
KEEP_RUNS=""
usage() {
cat <<'USAGE'
Usage: ./scripts/cleanup.sh [--execute] [--keep-runs N]
By default this is a dry-run. It removes old run_* directories beyond KEEP_RUNS
and clears tmp contents.
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--execute)
DRY_RUN=0
shift
;;
--keep-runs)
KEEP_RUNS="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
die "unknown option: $1"
;;
esac
done
load_env
keep="${KEEP_RUNS:-${RETAIN_RUNS:-100}}"
mapfile -t runs < <(find "$HOST_DATA_DIR/runs" -maxdepth 1 -type d -name 'run_*' 2>/dev/null | sort)
delete_count=$(( ${#runs[@]} - keep ))
if (( delete_count > 0 )); then
for ((i=0; i<delete_count; i++)); do
if [[ "$DRY_RUN" == "1" ]]; then
echo "DRY-RUN rm -rf ${runs[$i]}"
else
rm -rf "${runs[$i]}"
fi
done
fi
if [[ "$DRY_RUN" == "1" ]]; then
echo "DRY-RUN rm -rf $HOST_DATA_DIR/tmp/*"
else
find "$HOST_DATA_DIR/tmp" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
fi
df -h "$HOST_DATA_DIR" 2>/dev/null || true

View File

@@ -0,0 +1,597 @@
#!/usr/bin/env bash
set -euo pipefail
INSTALLER_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="${ENV_FILE:-$INSTALLER_ROOT/.env}"
ENV_EXAMPLE="$INSTALLER_ROOT/.env.example"
COMPOSE_FILE="$INSTALLER_ROOT/compose/docker-compose.yml"
MANIFEST_FILE="${MANIFEST_FILE:-$INSTALLER_ROOT/PACKAGE-MANIFEST.env}"
log() {
printf '[ours-rp-installer] %s\n' "$*"
}
warn() {
printf '[ours-rp-installer][WARN] %s\n' "$*" >&2
}
die() {
printf '[ours-rp-installer][ERROR] %s\n' "$*" >&2
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "missing command: $1"
}
normalize_arch() {
case "$1" in
amd64|x86_64|linux/amd64)
printf 'amd64\n'
;;
arm64|aarch64|linux/arm64)
printf 'arm64\n'
;;
*)
return 1
;;
esac
}
platform_for_arch() {
printf 'linux/%s\n' "$1"
}
env_file_has_key() {
local env_path="$1"
local key="$2"
[[ -f "$env_path" ]] && grep -Eq "^${key}=" "$env_path"
}
persist_env_default() {
local env_path="$1"
local key="$2"
local value="$3"
local tmp_env
if env_file_has_key "$env_path" "$key"; then
return 0
fi
tmp_env="$(mktemp)"
cat "$env_path" > "$tmp_env"
printf '%s=%s\n' "$key" "$value" >> "$tmp_env"
mv "$tmp_env" "$env_path"
}
env_flag_enabled() {
case "${1:-0}" in
1|true|TRUE|yes|YES|on|ON)
return 0
;;
*)
return 1
;;
esac
}
detect_host_arch() {
local raw_arch
raw_arch="$(uname -m)"
normalize_arch "$raw_arch" || die "unsupported host architecture: $raw_arch"
}
load_manifest() {
MANIFEST_PACKAGE_ARCH_RAW=""
MANIFEST_PACKAGE_PLATFORM_RAW=""
if [[ -f "$MANIFEST_FILE" ]]; then
set -a
# shellcheck disable=SC1090
source "$MANIFEST_FILE"
set +a
MANIFEST_PACKAGE_ARCH_RAW="${PACKAGE_ARCH:-${package_arch:-}}"
MANIFEST_PACKAGE_PLATFORM_RAW="${PACKAGE_PLATFORM:-${package_platform:-}}"
fi
}
effective_package_arch() {
if [[ -n "${PACKAGE_ARCH:-}" ]]; then
normalize_arch "$PACKAGE_ARCH" || die "unsupported PACKAGE_ARCH=${PACKAGE_ARCH}"
return 0
fi
if [[ -n "${PACKAGE_PLATFORM:-}" ]]; then
normalize_arch "$PACKAGE_PLATFORM" || die "unsupported PACKAGE_PLATFORM=${PACKAGE_PLATFORM}"
return 0
fi
if [[ -n "${RPKI_PLATFORM:-}" ]]; then
normalize_arch "$RPKI_PLATFORM" || die "unsupported RPKI_PLATFORM=${RPKI_PLATFORM}"
return 0
fi
if [[ -n "${METRICS_PLATFORM:-}" ]]; then
normalize_arch "$METRICS_PLATFORM" || die "unsupported METRICS_PLATFORM=${METRICS_PLATFORM}"
return 0
fi
if [[ -n "${MONITOR_PLATFORM:-}" ]]; then
normalize_arch "$MONITOR_PLATFORM" || die "unsupported MONITOR_PLATFORM=${MONITOR_PLATFORM}"
return 0
fi
die "unable to determine package architecture from manifest or env"
}
arch_mode() {
local host_arch package_arch
host_arch="$(detect_host_arch)"
package_arch="$(effective_package_arch)"
if [[ "$host_arch" == "$package_arch" ]]; then
printf 'native\n'
return 0
fi
if env_flag_enabled "${ALLOW_CROSS_ARCH:-0}"; then
printf 'cross-arch-enabled\n'
return 0
fi
printf 'mismatch-blocked\n'
}
assert_arch_compatibility() {
local host_arch package_arch package_platform mode
host_arch="$(detect_host_arch)"
package_arch="$(effective_package_arch)"
package_platform="${PACKAGE_PLATFORM:-${RPKI_PLATFORM:-$(platform_for_arch "$package_arch")}}"
mode="$(arch_mode)"
case "$mode" in
native)
return 0
;;
cross-arch-enabled)
log "cross-arch execution explicitly enabled: host_arch=$host_arch package_arch=$package_arch package_platform=$package_platform"
return 0
;;
mismatch-blocked)
die "package architecture mismatch: host_arch=$host_arch package_arch=$package_arch package_platform=$package_platform. Set ALLOW_CROSS_ARCH=1 in $ENV_FILE to allow explicit QEMU/binfmt emulation."
;;
*)
die "unknown arch compatibility mode: $mode"
;;
esac
}
load_env() {
load_manifest
if [[ ! -f "$ENV_FILE" ]]; then
[[ -f "$ENV_EXAMPLE" ]] || die "missing $ENV_EXAMPLE"
cp "$ENV_EXAMPLE" "$ENV_FILE"
log "created .env from .env.example"
fi
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
if [[ -n "${MANIFEST_PACKAGE_ARCH_RAW:-}" ]]; then
manifest_package_arch="$(normalize_arch "$MANIFEST_PACKAGE_ARCH_RAW")" || die "unsupported manifest package arch: $MANIFEST_PACKAGE_ARCH_RAW"
if [[ -n "${PACKAGE_ARCH:-}" ]]; then
env_package_arch="$(normalize_arch "$PACKAGE_ARCH")" || die "unsupported env package arch: $PACKAGE_ARCH"
[[ "$env_package_arch" == "$manifest_package_arch" ]] || die "env PACKAGE_ARCH=$env_package_arch mismatches manifest PACKAGE_ARCH=$manifest_package_arch"
fi
PACKAGE_ARCH="$manifest_package_arch"
else
if [[ -z "${PACKAGE_ARCH:-}" ]]; then
PACKAGE_ARCH="$(effective_package_arch)"
else
PACKAGE_ARCH="$(normalize_arch "$PACKAGE_ARCH")"
fi
fi
if [[ -n "${MANIFEST_PACKAGE_PLATFORM_RAW:-}" ]]; then
manifest_package_platform="$MANIFEST_PACKAGE_PLATFORM_RAW"
if [[ -n "${PACKAGE_PLATFORM:-}" && "$PACKAGE_PLATFORM" != "$manifest_package_platform" ]]; then
die "env PACKAGE_PLATFORM=$PACKAGE_PLATFORM mismatches manifest PACKAGE_PLATFORM=$manifest_package_platform"
fi
PACKAGE_PLATFORM="$manifest_package_platform"
else
PACKAGE_PLATFORM="${PACKAGE_PLATFORM:-$(platform_for_arch "$PACKAGE_ARCH")}"
fi
HOST_DATA_DIR="${HOST_DATA_DIR:-/var/lib/ours-rp-${PACKAGE_ARCH}-installer}"
COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-ours-rp-${PACKAGE_ARCH}-installer}"
RPKI_IMAGE="${RPKI_IMAGE:-ours-rp-runtime-${PACKAGE_ARCH}:${source_commit_short:-unknown}}"
RPKI_PLATFORM="${RPKI_PLATFORM:-$PACKAGE_PLATFORM}"
METRICS_IMAGE="${METRICS_IMAGE:-ours-rp-metrics-${PACKAGE_ARCH}:${source_commit_short:-unknown}}"
METRICS_PLATFORM="${METRICS_PLATFORM:-$PACKAGE_PLATFORM}"
RTR_REPORT_DIR="${RTR_REPORT_DIR:-$HOST_DATA_DIR/empty-rtr-report}"
RTR_REPORT_CONTAINER_DIR="${RTR_REPORT_CONTAINER_DIR:-/var/lib/ours-rp/rtr-report}"
MONITOR_PLATFORM="${MONITOR_PLATFORM:-$PACKAGE_PLATFORM}"
PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-prom/prometheus:v2.55.1}"
GRAFANA_IMAGE="${GRAFANA_IMAGE:-grafana/grafana:11.3.1}"
METRICS_INSTANCE="${METRICS_INSTANCE:-${PACKAGE_ARCH}-installer}"
ALLOW_CROSS_ARCH="${ALLOW_CROSS_ARCH:-0}"
RESOURCE_VALIDATION_MODE="${RESOURCE_VALIDATION_MODE:-validation-update-03}"
FIRST_RUN_WAIT_TIMEOUT_SECS="${FIRST_RUN_WAIT_TIMEOUT_SECS:-7200}"
persist_env_default "$ENV_FILE" "METRICS_IMAGE" "$METRICS_IMAGE"
persist_env_default "$ENV_FILE" "METRICS_PLATFORM" "$METRICS_PLATFORM"
persist_env_default "$ENV_FILE" "RTR_REPORT_DIR" "$RTR_REPORT_DIR"
persist_env_default "$ENV_FILE" "RTR_REPORT_CONTAINER_DIR" "$RTR_REPORT_CONTAINER_DIR"
persist_env_default "$ENV_FILE" "RESOURCE_VALIDATION_MODE" "$RESOURCE_VALIDATION_MODE"
}
compose_cmd() {
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" -p "$COMPOSE_PROJECT_NAME" "$@"
}
validate_rtr_report_dir() {
load_env
[[ -n "${RTR_REPORT_DIR:-}" ]] || return 0
[[ "$RTR_REPORT_DIR" = /* ]] || die "RTR_REPORT_DIR must be an absolute host path: $RTR_REPORT_DIR"
[[ -d "$RTR_REPORT_DIR" ]] || die "RTR_REPORT_DIR does not exist or is not a directory: $RTR_REPORT_DIR"
}
validate_rtr_registration_dir() {
local report_dir="$1"
[[ -n "$report_dir" ]] || die "--report-dir is required"
[[ "$report_dir" = /* ]] || die "--report-dir must be an absolute host path: $report_dir"
[[ "$report_dir" =~ ^/[A-Za-z0-9._/@:+,-]+$ ]] || die "--report-dir contains unsupported characters: $report_dir"
[[ -d "$report_dir" ]] || die "--report-dir does not exist: $report_dir"
[[ -r "$report_dir" ]] || die "--report-dir is not readable: $report_dir"
}
rtr_report_file_count() {
local report_dir="$1"
{
find "$report_dir" -maxdepth 1 -type f \( \
-name 'rtr-source-*.json' -o \
-name 'rtr-runtime-*.json' -o \
-name 'rtr-clients-*.json' \
\) -print 2>/dev/null || true
} | wc -l | tr -d '[:space:]'
}
latest_rtr_report_file() {
local report_dir="$1"
{
find "$report_dir" -maxdepth 1 -type f \( \
-name 'rtr-source-*.json' -o \
-name 'rtr-runtime-*.json' -o \
-name 'rtr-clients-*.json' \
\) -printf '%T@ %p\n' 2>/dev/null || true
} | sort -nr | awk 'NR == 1 { sub(/^[^ ]+ /, ""); print }'
}
replace_env_key_file() {
local env_path="$1"
local key="$2"
local value="$3"
local temp_path
[[ "$key" =~ ^[A-Z][A-Z0-9_]*$ ]] || die "invalid environment key: $key"
[[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] || die "environment value must be single-line"
temp_path="$(mktemp "${env_path}.tmp.XXXXXX")"
awk -v key="$key" -v value="$value" '
BEGIN { replaced = 0 }
$0 ~ "^" key "=" {
print key "=" value
replaced = 1
next
}
{ print }
END {
if (!replaced) {
print key "=" value
}
}
' "$env_path" > "$temp_path"
chmod --reference="$env_path" "$temp_path" 2>/dev/null || true
mv "$temp_path" "$env_path"
}
wait_for_endpoint() {
local url="$1"
local timeout_secs="$2"
local elapsed=0
while (( elapsed < timeout_secs )); do
if endpoint_ok "$url"; then
return 0
fi
sleep 2
elapsed=$((elapsed + 2))
done
return 1
}
create_data_dirs() {
load_env
mkdir -p \
"$HOST_DATA_DIR/state" \
"$HOST_DATA_DIR/runs" \
"$HOST_DATA_DIR/logs" \
"$HOST_DATA_DIR/tmp" \
"$HOST_DATA_DIR/empty-rtr-report" \
"$HOST_DATA_DIR/prometheus" \
"$HOST_DATA_DIR/grafana"
validate_rtr_report_dir
chmod 755 "$HOST_DATA_DIR" "$HOST_DATA_DIR/state" "$HOST_DATA_DIR/runs" "$HOST_DATA_DIR/logs" "$HOST_DATA_DIR/tmp" || true
chmod 777 "$HOST_DATA_DIR/prometheus" "$HOST_DATA_DIR/grafana" || true
}
latest_run_dir() {
load_env
find "$HOST_DATA_DIR/runs" -maxdepth 1 -mindepth 1 -type d -name 'run_*' 2>/dev/null | sort | tail -1
}
latest_success_run_dir() {
load_env
find "$HOST_DATA_DIR/runs" -maxdepth 2 -type f -path '*/run-summary.json' 2>/dev/null \
| while read -r summary; do
if jq -e '.status == "success"' "$summary" >/dev/null 2>&1; then
dirname "$summary"
fi
done | sort | tail -1
}
has_success_run() {
[[ -n "$(latest_success_run_dir)" ]]
}
print_run_summary() {
local run_dir="$1"
local summary="$run_dir/run-summary.json"
local meta="$run_dir/run-meta.json"
local timing="$run_dir/stage-timing.json"
local process_time="$run_dir/process-time.txt"
local vrps_file="$run_dir/vrps.csv"
local vaps_file="$run_dir/vaps.csv"
local status="unknown"
local sync_mode="unknown"
local wall_ms="null"
local validation_ms="null"
local repo_sync_ms="null"
local max_rss_kb="null"
local publication_points="null"
local vrps="null"
local vaps="null"
local warnings="null"
[[ -f "$summary" ]] || {
warn "missing run-summary.json in $run_dir"
return 1
}
status="$(jq -r '.status // "unknown"' "$summary" 2>/dev/null || echo unknown)"
wall_ms="$(jq -r '.wallMs // .wall_ms // "null"' "$summary" 2>/dev/null || echo null)"
warnings="$(jq -r '.warningCount // .warnings // "null"' "$summary" 2>/dev/null || echo null)"
if [[ -f "$meta" ]]; then
sync_mode="$(jq -r '.sync_mode // .syncMode // "unknown"' "$meta" 2>/dev/null || echo unknown)"
status="$(jq -r --arg fallback "$status" '.status // $fallback' "$meta" 2>/dev/null || echo "$status")"
fi
if [[ -f "$timing" ]]; then
validation_ms="$(jq -r '.validation_ms // "null"' "$timing" 2>/dev/null || echo null)"
repo_sync_ms="$(jq -r '.repo_sync_ms_total // "null"' "$timing" 2>/dev/null || echo null)"
publication_points="$(jq -r '.publication_points // "null"' "$timing" 2>/dev/null || echo null)"
fi
if [[ -f "$process_time" ]]; then
max_rss_kb="$(awk -F': ' '/Maximum resident set size/ {print $2; found=1} END {if (!found) print "null"}' "$process_time")"
fi
if [[ -f "$vrps_file" ]]; then
vrps="$(( $(wc -l < "$vrps_file") > 0 ? $(wc -l < "$vrps_file") - 1 : 0 ))"
fi
if [[ -f "$vaps_file" ]]; then
vaps="$(( $(wc -l < "$vaps_file") > 0 ? $(wc -l < "$vaps_file") - 1 : 0 ))"
fi
jq -n \
--arg run "$(basename "$run_dir")" \
--arg status "$status" \
--arg syncMode "$sync_mode" \
--argjson wallMs "$wall_ms" \
--argjson validationMs "$validation_ms" \
--argjson repoSyncMs "$repo_sync_ms" \
--argjson maxRssKb "$max_rss_kb" \
--argjson vrps "$vrps" \
--argjson vaps "$vaps" \
--argjson publicationPoints "$publication_points" \
--argjson warnings "$warnings" \
'{run:$run,status:$status,syncMode:$syncMode,wallMs:$wallMs,validationMs:$validationMs,repoSyncMs:$repoSyncMs,maxRssKb:$maxRssKb,vrps:$vrps,vaps:$vaps,publicationPoints:$publicationPoints,warnings:$warnings}'
}
wait_for_new_success_run() {
local before_latest="$1"
local timeout_secs="$2"
local start_epoch now run_dir summary meta status meta_status
start_epoch="$(date +%s)"
while true; do
run_dir="$(latest_run_dir || true)"
if [[ -n "$run_dir" && "$run_dir" != "$before_latest" ]]; then
summary="$run_dir/run-summary.json"
meta="$run_dir/run-meta.json"
if [[ -f "$summary" ]]; then
status="$(jq -r '.status // "unknown"' "$summary" 2>/dev/null || echo unknown)"
if [[ "$status" == "success" ]]; then
meta_status="unknown"
if [[ -f "$meta" ]]; then
meta_status="$(jq -r '.status // "unknown"' "$meta" 2>/dev/null || echo unknown)"
fi
if [[ "$meta_status" == "success" ]]; then
print_run_summary "$run_dir" || true
return 0
fi
fi
if [[ "$status" == "failed" || "$status" == "error" ]]; then
print_run_summary "$run_dir" || true
die "run failed: $run_dir"
fi
fi
fi
now="$(date +%s)"
if (( now - start_epoch > timeout_secs )); then
die "timed out waiting for first successful run after ${timeout_secs}s"
fi
sleep 10
done
}
docker_compose_available() {
docker compose version >/dev/null 2>&1
}
install_docker_if_missing() {
if command -v docker >/dev/null 2>&1 && docker_compose_available && command -v jq >/dev/null 2>&1 && command -v rsync >/dev/null 2>&1 && command -v curl >/dev/null 2>&1; then
log "docker and docker compose are already installed"
return 0
fi
if [[ "${SKIP_DEP_INSTALL:-0}" == "1" ]]; then
die "docker/docker compose missing and SKIP_DEP_INSTALL=1"
fi
if ! command -v apt-get >/dev/null 2>&1; then
die "docker/docker compose missing; automatic install currently supports apt-get only"
fi
log "installing missing runtime packages via apt"
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl jq rsync gzip tar docker.io
if ! docker_compose_available; then
if apt-cache show docker-compose-v2 >/dev/null 2>&1; then
DEBIAN_FRONTEND=noninteractive apt-get install -y docker-compose-v2
elif apt-cache show docker-compose-plugin >/dev/null 2>&1; then
DEBIAN_FRONTEND=noninteractive apt-get install -y docker-compose-plugin
elif apt-cache show docker-compose >/dev/null 2>&1; then
DEBIAN_FRONTEND=noninteractive apt-get install -y docker-compose
fi
fi
systemctl enable --now docker >/dev/null 2>&1 || true
docker_compose_available || die "docker compose is still unavailable after install"
}
load_installer_images() {
load_env
require_cmd docker
shopt -s nullglob
local image load_output desired_tag
local found=0
for image in "$INSTALLER_ROOT"/images/*.tar "$INSTALLER_ROOT"/images/*.tar.gz; do
found=1
log "loading docker image: $image"
if [[ "$image" == *.gz ]]; then
load_output="$(gzip -dc "$image" | docker load)"
else
load_output="$(docker load -i "$image")"
fi
printf '%s\n' "$load_output"
desired_tag=""
case "$(basename "$image")" in
"${image_tar:-}")
desired_tag="${image_tag:-$RPKI_IMAGE}"
;;
"${metrics_image_tar:-}")
desired_tag="${metrics_image:-$METRICS_IMAGE}"
;;
"${prometheus_image_tar:-}")
desired_tag="${prometheus_image:-$PROMETHEUS_IMAGE}"
;;
"${grafana_image_tar:-}")
desired_tag="${grafana_image:-$GRAFANA_IMAGE}"
;;
esac
if [[ -n "$desired_tag" ]]; then
ensure_loaded_image_tag "$desired_tag" "$load_output" "$image"
fi
done
shopt -u nullglob
(( found == 1 )) || warn "no image tar found under $INSTALLER_ROOT/images"
}
ensure_loaded_image_tag() {
local desired_tag="$1"
local load_output="$2"
local image_path="$3"
local loaded_ref=""
local loaded_id=""
local desired_id=""
loaded_ref="$(printf '%s\n' "$load_output" | awk -F'Loaded image: ' '/Loaded image: / {print $2; exit}')"
if [[ -z "$loaded_ref" ]]; then
loaded_ref="$(printf '%s\n' "$load_output" | awk -F'Loaded image ID: ' '/Loaded image ID: / {print $2; exit}')"
fi
[[ -n "$loaded_ref" ]] || die "unable to determine loaded image reference for $image_path"
loaded_id="$(docker image inspect --format '{{.Id}}' "$loaded_ref" 2>/dev/null || true)"
desired_id="$(docker image inspect --format '{{.Id}}' "$desired_tag" 2>/dev/null || true)"
if [[ -n "$loaded_id" && -n "$desired_id" && "$loaded_id" == "$desired_id" ]]; then
return 0
fi
log "tagging loaded image for package use: source=$loaded_ref target=$desired_tag"
docker tag "$loaded_ref" "$desired_tag"
}
ensure_binfmt_if_needed() {
require_cmd docker
load_env
local host_arch package_arch
host_arch="$(detect_host_arch)"
package_arch="$(effective_package_arch)"
if [[ "$host_arch" == "$package_arch" ]]; then
return 0
fi
assert_arch_compatibility
log "host arch is $host_arch; enabling binfmt/qemu for package arch $package_arch"
docker run --rm --privileged tonistiigi/binfmt --install "$package_arch"
}
verify_runtime_image() {
load_env
require_cmd docker
log "verifying runtime image $RPKI_IMAGE on $RPKI_PLATFORM"
verify_image_platform "$RPKI_IMAGE" "$RPKI_PLATFORM" "runtime"
verify_image_usage_output "$RPKI_IMAGE" "$RPKI_PLATFORM" "runtime" /opt/ours-rp/bin/rpki --help
}
verify_metrics_image() {
load_env
require_cmd docker
log "verifying metrics image $METRICS_IMAGE on $METRICS_PLATFORM"
verify_image_platform "$METRICS_IMAGE" "$METRICS_PLATFORM" "metrics"
verify_image_usage_output "$METRICS_IMAGE" "$METRICS_PLATFORM" "metrics" /opt/ours-rp/bin/rpki_artifact_metrics --help
}
verify_image_platform() {
local image="$1"
local expected_platform="$2"
local role="$3"
local actual_platform
docker image inspect "$image" >/dev/null
actual_platform="$(docker image inspect --format '{{.Os}}/{{.Architecture}}' "$image" 2>/dev/null || echo unknown)"
[[ "$actual_platform" == "$expected_platform" ]] || die "$role image platform mismatch: image=$image expected=$expected_platform actual=$actual_platform"
}
verify_image_usage_output() {
local image="$1"
local expected_platform="$2"
local role="$3"
shift 3
local help_path
local status
help_path="$(mktemp "${TMPDIR:-/tmp}/ours-rp-image-help.XXXXXX")"
set +e
docker run --rm --platform "$expected_platform" "$image" "$@" >"$help_path" 2>&1
status=$?
set -e
if [[ "$status" != "0" && "$status" != "1" ]]; then
cat "$help_path" >&2 || true
rm -f "$help_path"
die "$role image command failed: image=$image platform=$expected_platform exit=$status"
fi
grep -q '^Usage' "$help_path" || {
cat "$help_path" >&2 || true
rm -f "$help_path"
die "$role image did not emit usage output: image=$image platform=$expected_platform exit=$status"
}
head -5 "$help_path" || true
rm -f "$help_path"
}
verify_monitor_images() {
load_env
require_cmd docker
verify_image_platform "$PROMETHEUS_IMAGE" "$MONITOR_PLATFORM" "prometheus"
verify_image_platform "$GRAFANA_IMAGE" "$MONITOR_PLATFORM" "grafana"
}
endpoint_ok() {
local url="$1"
curl -fsS --max-time 5 "$url" >/dev/null 2>&1
}

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=common.sh
source "$SCRIPT_DIR/common.sh"
usage() {
cat <<'USAGE'
Usage: ./scripts/install.sh [--skip-dep-install]
Install or update the ours RP Docker installer package idempotently.
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--skip-dep-install)
export SKIP_DEP_INSTALL=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "unknown option: $1"
;;
esac
done
load_env
assert_arch_compatibility
install_docker_if_missing
require_cmd curl
require_cmd jq
require_cmd rsync
require_cmd gzip
require_cmd tar
create_data_dirs
load_installer_images
ensure_binfmt_if_needed
verify_runtime_image
verify_metrics_image
verify_monitor_images
compose_config_path="$(mktemp "${TMPDIR:-/tmp}/ours-rp-compose-config.XXXXXX.yml")"
compose_cmd --profile core --profile sidecar --profile monitor config >"$compose_config_path"
"$SCRIPT_DIR/self-check.sh" --quick
rm -f "$compose_config_path"
log "install complete"

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=common.sh
source "$SCRIPT_DIR/common.sh"
load_env
compose_cmd --profile core --profile sidecar --profile monitor logs "$@"

View File

@@ -0,0 +1,149 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
REPORT_DIR=""
DRY_RUN=0
CANDIDATE_ENV=""
CONFIG_FILE=""
ORIGINAL_ENV_FILE=""
ENV_BACKUP=""
REGISTRATION_APPLIED=0
usage() {
cat <<'EOF'
Usage: ./scripts/register_rtr_monitor.sh --report-dir <absolute-host-path> [--dry-run]
Registers one local RTR report directory for the artifact metrics sidecar.
The directory must be on the same host as this installer and may contain
rtr-source-*.json, rtr-runtime-*.json, and rtr-clients-*.json report files.
This command recreates only artifact-metrics, Prometheus, and Grafana. It does
not restart or recreate the ours-rp-soak container.
EOF
}
cleanup() {
[[ -n "$CANDIDATE_ENV" && -f "$CANDIDATE_ENV" ]] && rm -f "$CANDIDATE_ENV"
[[ -n "$CONFIG_FILE" && -f "$CONFIG_FILE" ]] && rm -f "$CONFIG_FILE"
return 0
}
trap cleanup EXIT
restore_failed_registration() {
if (( REGISTRATION_APPLIED == 0 )) || [[ -z "$ENV_BACKUP" || ! -f "$ENV_BACKUP" ]]; then
return 0
fi
warn "registration failed after .env replacement; restoring previous metrics configuration"
cp -p "$ENV_BACKUP" "$ORIGINAL_ENV_FILE"
ENV_FILE="$ORIGINAL_ENV_FILE"
load_env || warn "unable to reload restored .env"
compose_cmd --profile sidecar up -d --force-recreate artifact-metrics \
|| warn "unable to recreate artifact-metrics with restored configuration"
}
fail_after_apply() {
restore_failed_registration
die "$*"
}
while (( $# > 0 )); do
case "$1" in
--report-dir)
[[ $# -ge 2 ]] || die "--report-dir requires a value"
REPORT_DIR="$2"
shift 2
;;
--dry-run)
DRY_RUN=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "unknown argument: $1"
;;
esac
done
require_cmd docker
require_cmd curl
require_cmd grep
require_cmd jq
validate_rtr_registration_dir "$REPORT_DIR"
load_env
assert_arch_compatibility
ORIGINAL_ENV_FILE="$ENV_FILE"
REPORT_FILE_COUNT="$(rtr_report_file_count "$REPORT_DIR")"
LATEST_REPORT_FILE="$(latest_rtr_report_file "$REPORT_DIR")"
if (( REPORT_FILE_COUNT == 0 )); then
warn "no recognized RTR report JSON files are currently present in $REPORT_DIR"
fi
CANDIDATE_ENV="$(mktemp "$INSTALLER_ROOT/.env.rtr-register.XXXXXX")"
cp -p "$ORIGINAL_ENV_FILE" "$CANDIDATE_ENV"
replace_env_key_file "$CANDIDATE_ENV" "RTR_REPORT_DIR" "$REPORT_DIR"
ENV_FILE="$CANDIDATE_ENV"
load_env
CONFIG_FILE="$(mktemp "$INSTALLER_ROOT/.rtr-compose-config.XXXXXX")"
compose_cmd --profile sidecar --profile monitor config --format json > "$CONFIG_FILE"
jq -e --arg source "$REPORT_DIR" --arg target "$RTR_REPORT_CONTAINER_DIR" '
.services["artifact-metrics"].volumes
| any(.[]; .type == "bind" and .source == $source and .target == $target and .read_only == true)
' "$CONFIG_FILE" >/dev/null || die "candidate compose configuration is missing expected read-only RTR bind mount"
if (( DRY_RUN == 1 )); then
printf 'dry_run=ok\n'
printf 'report_dir=%s\n' "$REPORT_DIR"
printf 'recognized_report_files=%s\n' "$REPORT_FILE_COUNT"
printf 'latest_report=%s\n' "${LATEST_REPORT_FILE:--}"
printf 'expected_mount_source=%s\n' "$REPORT_DIR"
printf 'expected_mount_target=%s\n' "$RTR_REPORT_CONTAINER_DIR"
exit 0
fi
ENV_FILE="$ORIGINAL_ENV_FILE"
load_env
soak_container_before="$(compose_cmd ps -q ours-rp-soak || true)"
ENV_BACKUP="${ORIGINAL_ENV_FILE}.rtr-register-$(date -u +%Y%m%dT%H%M%SZ).bak"
cp -p "$ORIGINAL_ENV_FILE" "$ENV_BACKUP"
mv "$CANDIDATE_ENV" "$ORIGINAL_ENV_FILE"
CANDIDATE_ENV=""
REGISTRATION_APPLIED=1
load_env
compose_cmd --profile sidecar up -d --force-recreate artifact-metrics \
|| fail_after_apply "unable to recreate artifact-metrics"
compose_cmd --profile sidecar --profile monitor up -d prometheus grafana \
|| fail_after_apply "unable to start Prometheus and Grafana"
soak_container_after="$(compose_cmd ps -q ours-rp-soak || true)"
if [[ -n "$soak_container_before" && "$soak_container_before" != "$soak_container_after" ]]; then
fail_after_apply "ours-rp-soak container changed during RTR registration; investigate before continuing"
fi
wait_for_endpoint "http://127.0.0.1:${METRICS_PORT:-9556}/metrics" 60 \
|| fail_after_apply "artifact-metrics did not become ready"
wait_for_endpoint "http://127.0.0.1:${PROMETHEUS_PORT:-9090}/-/ready" 60 \
|| fail_after_apply "Prometheus did not become ready"
wait_for_endpoint "http://127.0.0.1:${GRAFANA_PORT:-3000}/api/health" 60 \
|| fail_after_apply "Grafana did not become ready"
if (( REPORT_FILE_COUNT > 0 )); then
curl -fsS "http://127.0.0.1:${METRICS_PORT:-9556}/metrics" | grep -q '^ours_rp_rtr_' \
|| fail_after_apply "artifact-metrics exposes no RTR metrics after registration"
fi
printf 'registration=ok\n'
printf 'report_dir=%s\n' "$REPORT_DIR"
printf 'recognized_report_files=%s\n' "$REPORT_FILE_COUNT"
printf 'latest_report=%s\n' "${LATEST_REPORT_FILE:--}"
printf 'env_backup=%s\n' "$ENV_BACKUP"
printf 'ours_rp_soak_container_unchanged=%s\n' "${soak_container_before:-not-running}"

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"$SCRIPT_DIR/stop.sh" || true
"$SCRIPT_DIR/start.sh" "$@"

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=common.sh
source "$SCRIPT_DIR/common.sh"
QUICK=0
while [[ $# -gt 0 ]]; do
case "$1" in
--quick)
QUICK=1
shift
;;
-h|--help)
echo "Usage: ./scripts/self-check.sh [--quick]"
exit 0
;;
*)
die "unknown option: $1"
;;
esac
done
load_env
assert_arch_compatibility
require_cmd docker
require_cmd jq
docker compose version >/dev/null
[[ -f "$COMPOSE_FILE" ]] || die "missing compose file"
[[ -f "$ENV_FILE" ]] || die "missing .env"
create_data_dirs
[[ -w "$HOST_DATA_DIR" ]] || die "data dir is not writable: $HOST_DATA_DIR"
compose_cmd --profile core --profile sidecar --profile monitor config >/dev/null
verify_image_platform "$RPKI_IMAGE" "$RPKI_PLATFORM" "runtime"
verify_image_platform "$METRICS_IMAGE" "$METRICS_PLATFORM" "metrics"
verify_monitor_images
if [[ "$QUICK" == "0" ]]; then
verify_runtime_image
verify_metrics_image
fi
log "self-check ok"

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=common.sh
source "$SCRIPT_DIR/common.sh"
WAIT_FIRST_RUN=1
TIMEOUT_SECS=""
usage() {
cat <<'USAGE'
Usage: ./scripts/start.sh [--no-wait-first-run] [--timeout-secs N]
Start ours RP. If no successful run exists, wait for the first snapshot to succeed
before starting metrics, Prometheus and Grafana.
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--no-wait-first-run)
WAIT_FIRST_RUN=0
shift
;;
--timeout-secs)
TIMEOUT_SECS="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
die "unknown option: $1"
;;
esac
done
load_env
assert_arch_compatibility
create_data_dirs
timeout_secs="${TIMEOUT_SECS:-$FIRST_RUN_WAIT_TIMEOUT_SECS}"
before_latest="$(latest_run_dir || true)"
had_success=0
if has_success_run; then
had_success=1
fi
log "starting core soak service"
compose_cmd --profile core up -d ours-rp-soak
if [[ "$had_success" == "0" && "$WAIT_FIRST_RUN" == "1" ]]; then
log "no previous successful run found; waiting for first run timeout=${timeout_secs}s"
wait_for_new_success_run "$before_latest" "$timeout_secs"
fi
log "starting metrics and monitor services"
compose_cmd --profile sidecar --profile monitor up -d artifact-metrics prometheus grafana
"$SCRIPT_DIR/status.sh" --brief || true

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=common.sh
source "$SCRIPT_DIR/common.sh"
BRIEF=0
while [[ $# -gt 0 ]]; do
case "$1" in
--brief)
BRIEF=1
shift
;;
-h|--help)
echo "Usage: ./scripts/status.sh [--brief]"
exit 0
;;
*)
die "unknown option: $1"
;;
esac
done
load_env
host_arch="$(detect_host_arch)"
mode="$(arch_mode)"
echo "installer_root=$INSTALLER_ROOT"
echo "manifest_file=$MANIFEST_FILE"
echo "package_name=${package_name:-}"
echo "source_commit=${source_commit:-${git_commit:-}}"
echo "source_commit_short=${source_commit_short:-}"
echo "source_dirty=${source_dirty:-}"
echo "build_timestamp_utc=${build_timestamp_utc:-${created_at_utc:-}}"
echo "host_data_dir=$HOST_DATA_DIR"
echo "host_arch=$host_arch"
echo "package_arch=$PACKAGE_ARCH"
echo "package_platform=$PACKAGE_PLATFORM"
echo "allow_cross_arch=$ALLOW_CROSS_ARCH"
echo "arch_mode=$mode"
echo "runtime_image=$RPKI_IMAGE"
echo "runtime_platform=$RPKI_PLATFORM"
echo "runtime_image_revision=${runtime_image_revision:-}"
echo "runtime_image_dirty=${runtime_image_dirty:-}"
echo "metrics_image=$METRICS_IMAGE"
echo "metrics_platform=$METRICS_PLATFORM"
echo "metrics_image_revision=${metrics_image_revision:-}"
echo "metrics_image_dirty=${metrics_image_dirty:-}"
echo "rtr_report_dir=$RTR_REPORT_DIR"
echo "rtr_report_container_dir=$RTR_REPORT_CONTAINER_DIR"
echo "monitor_platform=$MONITOR_PLATFORM"
echo "rirs=${RIRS:-}"
echo "max_runs=${MAX_RUNS:-}"
echo "interval_secs=${INTERVAL_SECS:-}"
echo "periodic_snapshot_reset=${PERIODIC_SNAPSHOT_RESET:-0}"
echo "periodic_snapshot_max_deltas=${PERIODIC_SNAPSHOT_MAX_DELTAS:-100}"
echo
if command -v docker >/dev/null 2>&1; then
docker version --format 'docker={{.Server.Version}}' 2>/dev/null || echo "docker=unavailable"
docker compose version 2>/dev/null || true
compose_cmd --profile core --profile sidecar --profile monitor ps || true
else
echo "docker=missing"
fi
echo
df -h "$HOST_DATA_DIR" 2>/dev/null || true
echo
latest="$(latest_run_dir || true)"
if [[ -n "$latest" ]]; then
echo "latest_run=$latest"
print_run_summary "$latest" || true
else
echo "latest_run=none"
fi
if [[ "$BRIEF" == "0" ]]; then
echo
endpoint_ok "http://127.0.0.1:${METRICS_PORT:-9556}/metrics" && echo "metrics=ok" || echo "metrics=unavailable"
endpoint_ok "http://127.0.0.1:${PROMETHEUS_PORT:-9090}/-/ready" && echo "prometheus=ok" || echo "prometheus=unavailable"
endpoint_ok "http://127.0.0.1:${GRAFANA_PORT:-3000}/api/health" && echo "grafana=ok" || echo "grafana=unavailable"
echo
if [[ "$RTR_REPORT_DIR" == "$HOST_DATA_DIR/empty-rtr-report" ]]; then
echo "rtr_registration=fallback-empty-directory"
else
echo "rtr_registration=registered"
fi
echo "rtr_report_files=$(rtr_report_file_count "$RTR_REPORT_DIR")"
echo "rtr_latest_report=$(latest_rtr_report_file "$RTR_REPORT_DIR" || true)"
fi

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=common.sh
source "$SCRIPT_DIR/common.sh"
load_env
compose_cmd --profile core --profile sidecar --profile monitor stop "$@"

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=common.sh
source "$SCRIPT_DIR/common.sh"
PURGE_DATA=0
while [[ $# -gt 0 ]]; do
case "$1" in
--purge-data)
PURGE_DATA=1
shift
;;
-h|--help)
echo "Usage: ./scripts/uninstall.sh [--purge-data]"
exit 0
;;
*)
die "unknown option: $1"
;;
esac
done
load_env
compose_cmd --profile core --profile sidecar --profile monitor down --remove-orphans || true
if [[ "$PURGE_DATA" == "1" ]]; then
[[ "$HOST_DATA_DIR" == "/" || -z "$HOST_DATA_DIR" ]] && die "refuse to purge unsafe HOST_DATA_DIR=$HOST_DATA_DIR"
rm -rf "$HOST_DATA_DIR"
log "purged data dir $HOST_DATA_DIR"
else
log "containers removed; data kept at $HOST_DATA_DIR"
fi

View File

@@ -0,0 +1,110 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=common.sh
source "$SCRIPT_DIR/common.sh"
REUSE_ENV_FROM=""
UPDATE_PACKAGE_IMAGE=1
usage() {
cat <<'USAGE'
Usage: ./scripts/upgrade.sh [--reuse-env-from /path/to/.env] [--keep-reused-image]
By default, --reuse-env-from creates the new .env from this package's
.env.example first, then overlays existing user settings from the old .env.
Image tags are intentionally kept from the new package so the upgraded service
actually runs the new packaged runtime and monitor images. Use
--keep-reused-image only when you intentionally want to keep previous image tags.
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--reuse-env-from)
REUSE_ENV_FROM="$2"
shift 2
;;
--keep-reused-image)
UPDATE_PACKAGE_IMAGE=0
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "unknown option: $1"
;;
esac
done
env_get_key() {
local env_path="$1"
local key="$2"
awk -F= -v key="$key" '$1 == key {sub(/^[^=]*=/, ""); print; exit}' "$env_path"
}
env_set_key() {
local env_path="$1"
local key="$2"
local value="$3"
local tmp_env
if grep -q "^${key}=" "$env_path"; then
tmp_env="$(mktemp)"
awk -v key="$key" -v value="$value" '
BEGIN { done=0 }
$0 ~ "^" key "=" { print key "=" value; done=1; next }
{ print }
END { if (!done) print key "=" value }
' "$env_path" > "$tmp_env"
mv "$tmp_env" "$env_path"
else
printf '%s=%s\n' "$key" "$value" >> "$env_path"
fi
}
overlay_reused_env() {
local source_env="$1"
local target_env="$2"
local key
local value
while IFS='=' read -r key _; do
[[ -n "$key" ]] || continue
[[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
case "$key" in
PACKAGE_ARCH|PACKAGE_PLATFORM|RPKI_PLATFORM|METRICS_PLATFORM|MONITOR_PLATFORM)
continue
;;
RPKI_IMAGE|METRICS_IMAGE|PROMETHEUS_IMAGE|GRAFANA_IMAGE)
[[ "$UPDATE_PACKAGE_IMAGE" == "0" ]] || continue
;;
esac
value="$(env_get_key "$source_env" "$key")"
env_set_key "$target_env" "$key" "$value"
done < <(grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$source_env" || true)
}
if [[ -n "$REUSE_ENV_FROM" ]]; then
[[ -f "$REUSE_ENV_FROM" ]] || die "missing reuse env file: $REUSE_ENV_FROM"
if [[ ! -f "$ENV_FILE" ]]; then
[[ -f "$ENV_EXAMPLE" ]] || die "missing $ENV_EXAMPLE"
cp "$ENV_EXAMPLE" "$ENV_FILE"
overlay_reused_env "$REUSE_ENV_FROM" "$ENV_FILE"
log "created new package env from .env.example and overlaid user settings from $REUSE_ENV_FROM"
else
log "keeping existing env at $ENV_FILE; reuse source ignored: $REUSE_ENV_FROM"
fi
fi
load_env
assert_arch_compatibility
create_data_dirs
install_docker_if_missing
load_installer_images
ensure_binfmt_if_needed
verify_runtime_image
verify_metrics_image
verify_monitor_images
compose_cmd --profile core --profile sidecar --profile monitor up -d --force-recreate
"$SCRIPT_DIR/status.sh" --brief || true

View File

@@ -0,0 +1,117 @@
ARG BUILDER_IMAGE=rust:1-bookworm
ARG RUNTIME_IMAGE=debian:bookworm-slim
FROM --platform=$BUILDPLATFORM ${BUILDER_IMAGE} AS builder
ARG BUILDARCH
ARG TARGETARCH
ARG TARGETPLATFORM
WORKDIR /src
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
clang \
cmake \
git \
libclang-dev \
make \
perl \
pkg-config \
python3 \
&& if [ "$TARGETARCH" != "$BUILDARCH" ]; then \
case "$TARGETARCH" in \
arm64) \
apt-get install -y --no-install-recommends \
g++-aarch64-linux-gnu \
gcc-aarch64-linux-gnu \
libc6-dev-arm64-cross \
;; \
amd64) \
apt-get install -y --no-install-recommends \
g++-x86-64-linux-gnu \
gcc-x86-64-linux-gnu \
libc6-dev-amd64-cross \
;; \
*) \
echo "unsupported TARGETARCH=$TARGETARCH" >&2; exit 2 \
;; \
esac; \
fi \
&& rm -rf /var/lib/apt/lists/* \
&& case "$TARGETARCH" in \
amd64|arm64) ;; \
*) echo "unsupported TARGETARCH=$TARGETARCH" >&2; exit 2 ;; \
esac
COPY . .
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/src/target \
set -eux; \
case "$TARGETARCH" in \
amd64) target_triple=x86_64-unknown-linux-gnu ;; \
arm64) target_triple=aarch64-unknown-linux-gnu ;; \
*) echo "unsupported TARGETARCH=$TARGETARCH" >&2; exit 2 ;; \
esac; \
rustup target add "$target_triple"; \
if [ "$TARGETARCH" != "$BUILDARCH" ]; then \
case "$TARGETARCH" in \
arm64) \
export \
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc \
CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++ \
AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar \
PKG_CONFIG_ALLOW_CROSS=1 \
;; \
amd64) \
export \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc \
CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc \
CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++ \
AR_x86_64_unknown_linux_gnu=x86_64-linux-gnu-ar \
PKG_CONFIG_ALLOW_CROSS=1 \
;; \
*) \
echo "unsupported TARGETARCH=$TARGETARCH" >&2; exit 2 \
;; \
esac; \
fi; \
cargo build --release --target "$target_triple" --bin rpki_artifact_metrics; \
mkdir -p /build-out/bin; \
cp "target/$target_triple/release/rpki_artifact_metrics" /build-out/bin/
FROM --platform=$TARGETPLATFORM ${RUNTIME_IMAGE} AS runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
tzdata \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/ours-rp
COPY --from=builder /build-out/bin/ /opt/ours-rp/bin/
ARG SOURCE_COMMIT=unknown
ARG SOURCE_DIRTY=unknown
ARG BUILD_TIMESTAMP_UTC=unknown
LABEL org.opencontainers.image.title="ours-rp-metrics" \
org.opencontainers.image.description="Ours RP artifact metrics image for multi-arch Docker Compose deployment" \
org.opencontainers.image.revision="${SOURCE_COMMIT}" \
org.opencontainers.image.created="${BUILD_TIMESTAMP_UTC}" \
org.opencontainers.image.source-dirty="${SOURCE_DIRTY}"
RUN chmod +x /opt/ours-rp/bin/* \
&& mkdir -p /var/lib/ours-rp/state /var/lib/ours-rp/runs /var/lib/ours-rp/logs
ENV RUN_ROOT=/var/lib/ours-rp \
BIN_DIR=/opt/ours-rp/bin \
RUST_BACKTRACE=1
VOLUME ["/var/lib/ours-rp"]

View File

@@ -0,0 +1,145 @@
ARG BUILDER_IMAGE=rust:1-bookworm
ARG RUNTIME_IMAGE=debian:bookworm-slim
FROM --platform=$BUILDPLATFORM ${BUILDER_IMAGE} AS builder
ARG BUILDARCH
ARG TARGETARCH
ARG TARGETPLATFORM
WORKDIR /src
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
clang \
cmake \
git \
libclang-dev \
make \
perl \
pkg-config \
python3 \
&& if [ "$TARGETARCH" != "$BUILDARCH" ]; then \
case "$TARGETARCH" in \
arm64) \
apt-get install -y --no-install-recommends \
g++-aarch64-linux-gnu \
gcc-aarch64-linux-gnu \
libc6-dev-arm64-cross \
;; \
amd64) \
apt-get install -y --no-install-recommends \
g++-x86-64-linux-gnu \
gcc-x86-64-linux-gnu \
libc6-dev-amd64-cross \
;; \
*) \
echo "unsupported TARGETARCH=$TARGETARCH" >&2; exit 2 \
;; \
esac; \
fi \
&& rm -rf /var/lib/apt/lists/* \
&& case "$TARGETARCH" in \
amd64|arm64) ;; \
*) echo "unsupported TARGETARCH=$TARGETARCH" >&2; exit 2 ;; \
esac
COPY . .
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/src/target \
set -eux; \
case "$TARGETARCH" in \
amd64) target_triple=x86_64-unknown-linux-gnu ;; \
arm64) target_triple=aarch64-unknown-linux-gnu ;; \
*) echo "unsupported TARGETARCH=$TARGETARCH" >&2; exit 2 ;; \
esac; \
rustup target add "$target_triple"; \
if [ "$TARGETARCH" != "$BUILDARCH" ]; then \
case "$TARGETARCH" in \
arm64) \
export \
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc \
CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++ \
AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar \
PKG_CONFIG_ALLOW_CROSS=1 \
;; \
amd64) \
export \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc \
CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc \
CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++ \
AR_x86_64_unknown_linux_gnu=x86_64-linux-gnu-ar \
PKG_CONFIG_ALLOW_CROSS=1 \
;; \
*) \
echo "unsupported TARGETARCH=$TARGETARCH" >&2; exit 2 \
;; \
esac; \
fi; \
cargo build --release --target "$target_triple" \
--bin rpki \
--bin rpki_daemon \
--bin db_stats; \
mkdir -p /build-out/bin; \
cp \
"target/$target_triple/release/rpki" \
"target/$target_triple/release/rpki_daemon" \
"target/$target_triple/release/db_stats" \
/build-out/bin/
FROM --platform=$TARGETPLATFORM ${RUNTIME_IMAGE} AS runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
bash \
ca-certificates \
coreutils \
curl \
findutils \
iputils-ping \
jq \
procps \
python3 \
rsync \
time \
tzdata \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/ours-rp
COPY --from=builder /build-out/bin/ /opt/ours-rp/bin/
COPY scripts/soak/run_soak.sh /opt/ours-rp/run_soak.sh
COPY scripts/soak/portable-soak.env.example /opt/ours-rp/portable-soak.env.example
COPY tests/fixtures/tal/ /opt/ours-rp/fixtures/tal/
COPY tests/fixtures/ta/ /opt/ours-rp/fixtures/ta/
COPY fixtures/live_20260619/tal/ /opt/ours-rp/fixtures/live_20260619/tal/
COPY fixtures/live_20260619/ta/ /opt/ours-rp/fixtures/live_20260619/ta/
ARG SOURCE_COMMIT=unknown
ARG SOURCE_DIRTY=unknown
ARG BUILD_TIMESTAMP_UTC=unknown
LABEL org.opencontainers.image.title="ours-rp-runtime" \
org.opencontainers.image.description="Ours RP runtime image for multi-arch Docker Compose deployment" \
org.opencontainers.image.revision="${SOURCE_COMMIT}" \
org.opencontainers.image.created="${BUILD_TIMESTAMP_UTC}" \
org.opencontainers.image.source-dirty="${SOURCE_DIRTY}"
RUN chmod +x /opt/ours-rp/run_soak.sh /opt/ours-rp/bin/* \
&& mkdir -p /var/lib/ours-rp/state /var/lib/ours-rp/runs /var/lib/ours-rp/logs /var/lib/ours-rp/tmp
ENV PACKAGE_ROOT=/opt/ours-rp \
ENV_FILE=/opt/ours-rp/.env \
RUN_ROOT=/var/lib/ours-rp \
BIN_DIR=/opt/ours-rp/bin \
FIXTURE_DIR=/opt/ours-rp/fixtures \
RUST_BACKTRACE=1
VOLUME ["/var/lib/ours-rp"]
CMD ["/opt/ours-rp/run_soak.sh"]

View File

@@ -0,0 +1,80 @@
{
"created_at_utc": "2026-06-19T08:08:03Z",
"items": [
{
"rir": "afrinic",
"ta_bytes": 1216,
"ta_download": "200 1216 1.351147",
"ta_elapsed_s": 1.227,
"ta_path": "rpki_2/rpki/fixtures/live_20260619/ta/afrinic-ta.cer",
"ta_sha256": "43a26fd28bafb9398e5b2ab19e036b450bd04f4973a7f5ad151cebdee0edac36",
"ta_uri": "https://rpki.afrinic.net/repository/AfriNIC.cer",
"tal_bytes": 496,
"tal_download": "200 496 1.361326",
"tal_elapsed_s": 1.238,
"tal_path": "rpki_2/rpki/fixtures/live_20260619/tal/afrinic.tal",
"tal_sha256": "2838ef30ea27ce5705abf5f5adb131d8c35b1f50858338a2f3c84bb207c2fa35",
"tal_url": "https://rpki.afrinic.net/tal/afrinic.tal"
},
{
"rir": "apnic",
"ta_bytes": 1222,
"ta_download": "200 1222 1.012321",
"ta_elapsed_s": 0.921,
"ta_path": "rpki_2/rpki/fixtures/live_20260619/ta/apnic-ta.cer",
"ta_sha256": "2014230ad49b2777ac2bde0948ddfa4b8f207114c549e26d755de88c3593e3af",
"ta_uri": "https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer",
"tal_bytes": 532,
"tal_download": "200 466 1.007155",
"tal_elapsed_s": 0.917,
"tal_path": "rpki_2/rpki/fixtures/live_20260619/tal/apnic.tal",
"tal_sha256": "472e551f7c551c2e999e582b7c9437d3bee4900fe53afff62aeb28d4940ade94",
"tal_url": "https://tal.apnic.net/apnic.tal"
},
{
"rir": "arin",
"ta_bytes": 1143,
"ta_download": "200 1143 0.816714",
"ta_elapsed_s": 0.743,
"ta_path": "rpki_2/rpki/fixtures/live_20260619/ta/arin-ta.cer",
"ta_sha256": "5b3c2f6f04abd19261084487a43c156f778b0b8926d63801d48c7a93ed349492",
"ta_uri": "https://rrdp.arin.net/arin-rpki-ta.cer",
"tal_bytes": 1258,
"tal_download": "200 1258 0.848581",
"tal_elapsed_s": 0.774,
"tal_path": "rpki_2/rpki/fixtures/live_20260619/tal/arin.tal",
"tal_sha256": "1f8bdb03bcc30a3b8e11fd9a87102fba250c22137a3c8baa9c81b139cb412639",
"tal_url": "https://www.arin.net/resources/manage/rpki/arin.tal"
},
{
"rir": "lacnic",
"ta_bytes": 1166,
"ta_download": "200 1166 1.025273",
"ta_elapsed_s": 0.932,
"ta_path": "rpki_2/rpki/fixtures/live_20260619/ta/lacnic-ta.cer",
"ta_sha256": "f44bc51008fd6998de7597b72a79b07bf3ebcb0f14daa7c7c022c0e9d66e0ad0",
"ta_uri": "https://rrdp.lacnic.net/ta/rta-lacnic-rpki.cer",
"tal_bytes": 502,
"tal_download": "200 502 1.729122",
"tal_elapsed_s": 1.565,
"tal_path": "rpki_2/rpki/fixtures/live_20260619/tal/lacnic.tal",
"tal_sha256": "d44bb9394ab009c8b53e5efebf2a1c9450bab61a27efe00de5a3e4587a3a2f6a",
"tal_url": "https://www.lacnic.net/innovaportal/file/4983/1/lacnic.tal"
},
{
"rir": "ripe",
"ta_bytes": 1036,
"ta_download": "200 1036 1.060992",
"ta_elapsed_s": 4.536,
"ta_path": "rpki_2/rpki/fixtures/live_20260619/ta/ripe-ncc-ta.cer",
"ta_sha256": "3e3f7e4efc8d0cea03d9cc1fde6e168b45c26d7b3272e14abd8da4871886e539",
"ta_uri": "https://rpki.ripe.net/ta/ripe-ncc-ta.cer",
"tal_bytes": 482,
"tal_download": "200 482 1.092082",
"tal_elapsed_s": 0.992,
"tal_path": "rpki_2/rpki/fixtures/live_20260619/tal/ripe-ncc.tal",
"tal_sha256": "59ca27ef93f23682749fcefe7c6d70fbc723343549ff9e4d3996acaff79817fb",
"tal_url": "https://tal.rpki.ripe.net/ripe-ncc.tal"
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,10 @@
rsync://rpki.afrinic.net/repository/AfriNIC.cer
https://rpki.afrinic.net/repository/AfriNIC.cer
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxsAqAhWIO+ON2Ef9oRDM
pKxv+AfmSLIdLWJtjrvUyDxJPBjgR+kVrOHUeTaujygFUp49tuN5H2C1rUuQavTH
vve6xNF5fU3OkTcqEzMOZy+ctkbde2SRMVdvbO22+TH9gNhKDc9l7Vu01qU4LeJH
k3X0f5uu5346YrGAOSv6AaYBXVgXxa0s9ZvgqFpim50pReQe/WI3QwFKNgpPzfQL
6Y7fDPYdYaVOXPXSKtx7P4s4KLA/ZWmRL/bobw/i2fFviAGhDrjqqqum+/9w1hEl
L/vqihVnV18saKTnLvkItA/Bf5i11Yhw2K7qv573YWxyuqCknO/iYLTR1DToBZcZ
UQIDAQAB

View File

@@ -0,0 +1,10 @@
https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer
rsync://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RWSL61YAAYumEiU8z8
qH2ETVIL01ilxZlzIL9JYSORMN5Cmtf8V2JblIealSqgOTGjvSjEsiV73s67zYQI
7C/iSOb96uf3/s86NqbxDiFQGN8qG7RNcdgVuUlAidl8WxvLNI8VhqbAB5uSg/Mr
LeSOvXRja041VptAxIhcGzDMvlAJRwkrYK/Mo8P4E2rSQgwqCgae0ebY1CsJ3Cjf
i67C1nw7oXqJJovvXJ4apGmEv8az23OLC6Ki54Ul/E6xk227BFttqFV3YMtKx42H
cCcDVZZy01n7JjzvO8ccaXmHIgR7utnqhBRNNq5Xc5ZhbkrUsNtiJmrZzVlgU6Ou
0wIDAQAB

View File

@@ -0,0 +1,10 @@
https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer
rsync://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RWSL61YAAYumEiU8z8
qH2ETVIL01ilxZlzIL9JYSORMN5Cmtf8V2JblIealSqgOTGjvSjEsiV73s67zYQI
7C/iSOb96uf3/s86NqbxDiFQGN8qG7RNcdgVuUlAidl8WxvLNI8VhqbAB5uSg/Mr
LeSOvXRja041VptAxIhcGzDMvlAJRwkrYK/Mo8P4E2rSQgwqCgae0ebY1CsJ3Cjf
i67C1nw7oXqJJovvXJ4apGmEv8az23OLC6Ki54Ul/E6xk227BFttqFV3YMtKx42H
cCcDVZZy01n7JjzvO8ccaXmHIgR7utnqhBRNNq5Xc5ZhbkrUsNtiJmrZzVlgU6Ou
0wIDAQAB

View File

@@ -0,0 +1,19 @@
# THIS TRUST ANCHOR LOCATOR IS PROVIDED BY THE AMERICAN REGISTRY FOR
# INTERNET NUMBERS (ARIN) "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL ARIN BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS PUBLIC KEY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
rsync://rpki.arin.net/repository/arin-rpki-ta.cer
https://rrdp.arin.net/arin-rpki-ta.cer
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3lZPjbHvMRV5sDDqfLc/685th5FnreHMJjg8
pEZUbG8Y8TQxSBsDebbsDpl3Ov3Cj1WtdrJ3CIfQODCPrrJdOBSrMATeUbPC+JlNf2SRP3UB+VJFgtTj
0RN8cEYIuhBW5t6AxQbHhdNQH+A1F/OJdw0q9da2U29Lx85nfFxvnC1EpK9CbLJS4m37+RlpNbT1cba+
b+loXpx0Qcb1C4UpJCGDy7uNf5w6/+l7RpATAHqqsX4qCtwwDYlbHzp2xk9owF3mkCxzl0HwncO+sEHH
eaL3OjtwdIGrRGeHi2Mpt+mvWHhtQqVG+51MHTyg+nIjWFKKGx1Q9+KDx4wJStwveQIDAQAB

View File

@@ -0,0 +1,4 @@
https://rrdp.lacnic.net/ta/rta-lacnic-rpki.cer
rsync://repository.lacnic.net/rpki/lacnic/rta-lacnic-rpki.cer
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqZEzhYK0+PtDOPfub/KRc3MeWx3neXx4/wbnJWGbNAtbYqXg3uU5J4HFzPgk/VIppgSKAhlO0H60DRP48by9gr5/yDHu2KXhOmnMg46sYsUIpfgtBS9+VtrqWziJfb+pkGtuOWeTnj6zBmBNZKK+5AlMCW1WPhrylIcB+XSZx8tk9GS/3SMQ+YfMVwwAyYjsex14Uzto4GjONALE5oh1M3+glRQduD6vzSwOD+WahMbc9vCOTED+2McLHRKgNaQf0YJ9a1jG9oJIvDkKXEqdfqDRktwyoD74cV57bW3tBAexB7GglITbInyQAsmdngtfg2LUMrcROHHP86QPZINjDQIDAQAB

View File

@@ -0,0 +1,10 @@
https://rpki.ripe.net/ta/ripe-ncc-ta.cer
rsync://rpki.ripe.net/ta/ripe-ncc-ta.cer
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0URYSGqUz2myBsOzeW1j
Q6NsxNvlLMyhWknvnl8NiBCs/T/S2XuNKQNZ+wBZxIgPPV2pFBFeQAvoH/WK83Hw
A26V2siwm/MY2nKZ+Olw+wlpzlZ1p3Ipj2eNcKrmit8BwBC8xImzuCGaV0jkRB0G
Z0hoH6Ml03umLprRsn6v0xOP0+l6Qc1ZHMFVFb385IQ7FQQTcVIxrdeMsoyJq9eM
kE6DoclHhF/NlSllXubASQ9KUWqJ0+Ot3QCXr4LXECMfkpkVR2TZT+v5v658bHVs
6ZxRD1b6Uk1uQKAyHUbn/tXvP8lrjAibGzVsXDT2L0x4Edx+QdixPgOji3gBMyL2
VwIDAQAB

220
monitor/README.md Normal file
View File

@@ -0,0 +1,220 @@
# Ours RP Prometheus / Grafana Monitor
本目录提供本地开发监控栈,用于采集 `rpki_artifact_metrics` 暴露的 ours RP soak 指标。
## 前置条件
1. Docker + Docker Compose v2;
2. 宿主机已启动 `rpki_artifact_metrics`,并监听 Docker 网桥可访问的地址,例如 `0.0.0.0:9556`;
3. Prometheus 容器通过 `host.docker.internal:9556` 访问宿主 sidecar。
Linux Docker 下 compose 已配置:
```yaml
extra_hosts:
- host.docker.internal:host-gateway
```
## 启动
```bash
cd rpki_2/rpki/monitor
docker compose up -d
```
默认镜像使用官方 Docker Hub 镜像:
```text
prom/prometheus:v2.55.1
grafana/grafana:11.3.1
```
如需切到其它镜像源:
```bash
PROMETHEUS_IMAGE=<mirror>/prom/prometheus:v2.55.1 \
GRAFANA_IMAGE=<mirror>/grafana/grafana:11.3.1 \
docker compose up -d
```
默认端口:
- Prometheus: <http://localhost:9090>
- Grafana: <http://localhost:3000>
- Grafana 默认账号密码:`admin` / `admin`
如端口冲突:
```bash
PROMETHEUS_PORT=19090 GRAFANA_PORT=13000 docker compose up -d
```
Prometheus 默认保留 7 天数据;可通过 `PROMETHEUS_RETENTION` 覆盖:
```bash
PROMETHEUS_RETENTION=7d docker compose up -d
```
## 长期稳定性测试
portable soak package 内置 `run_24h_soak_with_metrics.sh`,用于连续运行 ours RP、启动 metrics sidecar、启动本监控栈,并每小时生成报告:
```bash
cd /path/to/portable-soak
SOAK_DURATION_SECS=0 \
HOURLY_REPORT_INTERVAL_SECS=3600 \
SOAK_RETAIN_RUNS=100 \
CLEAN_TMP_AFTER_RUN=1 \
PROMETHEUS_RETENTION=7d \
STOP_MONITOR_STACK_ON_EXIT=0 \
FEISHU_WEBHOOK_SCRIPT=/home/yuyr/.codex/skills/user/feishu-webhook/scripts/send_feishu_text.py \
./run_24h_soak_with_metrics.sh
```
`SOAK_DURATION_SECS=0` 表示持续运行不自动停止;如需 24 小时自然停止,可设置为 `86400`,脚本会等当前 run 完成后退出,不会直接 kill 半轮验证。
关键产物:
- `runs/run_xxxx/`:最近 100 个 run 原始产物;
- `hourly_reports/hour_*.md`:小时级报告;
- `hourly_reports/hourly_summary.jsonl`:小时级结构化汇总;
- `incident_runs/run_xxxx/`:异常 run 固化副本;
- `logs/metrics.*`、`logs/24h-soak.*`、`logs/hourly-reporter.*`:运行日志。
短周期联调可把 `SOAK_DURATION_SECS` 和 `HOURLY_REPORT_INTERVAL_SECS` 调小,并设置 `FEISHU_DRY_RUN=1` 避免真实飞书通知。
## 停止
```bash
cd rpki_2/rpki/monitor
docker compose down
```
保留数据 volume。若要清理数据:
```bash
docker compose down -v
```
## 典型本地联调命令
先启动 APNIC soak 和 metrics sidecar,例如:
```bash
# soak .env 关键配置
MAX_RUNS=-1
RIRS=apnic
RETAIN_RUNS=5
INTERVAL_SECS=0
# metrics sidecar
rpki_artifact_metrics \
--run-root /path/to/portable-soak \
--listen 0.0.0.0:9556 \
--poll-secs 5 \
--instance local-apnic-continuous
```
再启动监控栈:
```bash
cd rpki_2/rpki/monitor
docker compose up -d
```
## 验证
Prometheus target:
```bash
curl -s 'http://localhost:9090/api/v1/targets' | python3 -m json.tool
```
Prometheus query:
```bash
curl -G 'http://localhost:9090/api/v1/query' \
--data-urlencode 'query=up{job="ours-rp-artifact-metrics"}'
curl -G 'http://localhost:9090/api/v1/query' \
--data-urlencode 'query=ours_rp_run_completed_total{status="success"}'
```
Grafana health:
```bash
curl -s http://localhost:3000/api/health | python3 -m json.tool
```
Grafana dashboard:
- 打开 <http://localhost:3000/d/ours-rp-soak-overview/ours-rp-soak-overview>
## 主要指标
- `ours_rp_metrics_service_up`
- `ours_rp_run_completed_total`
- `ours_rp_run_duration_seconds`
- `ours_rp_run_max_rss_bytes`
- `ours_rp_vrps{kind="total|unique"}`:`total` 为去重前 VRP 条目数,`unique` 按 `(ASN, IP Prefix, Max Length)` 去重。
- `ours_rp_vaps`
- `ours_rp_publication_points`
- `ours_rp_repo_sync_phase_count`
- `ours_rp_large_publication_points{object_count_gt="10|50|100|..."}`
- `ours_rp_cir_objects`
- `ours_rp_ccr_state_items`
## Inter-RP 持续对比监控
`rpki_inter_rp_metrics` 用于汇总三方 RP 的最新产物:
- ours RP:读取当前 portable soak 的 `runs/run_xxxx/run-summary.json`、`result.ccr`、CSV 产物;
- Routinator:读取远端200同步来的 `routinator/latest/run-meta.json`、`vrps.csv`、`vaps.csv`;
- rpki-client 9.8:读取远端200同步来的 `rpki-client/latest/run-meta.json`、`vrps.csv`、`vaps.csv`、`result.ccr`。
远端231 启动 sidecar 示例:
```bash
rpki_inter_rp_metrics \
--ours-run-root /root/rpki_20260608_2_feature062_24h_20260608T075547Z/portable-soak \
--peer-root /root/inter-rp-aggregator/synced-from-200 \
--listen 0.0.0.0:9557 \
--poll-secs 30 \
--instance remote231-inter-rp
```
Prometheus 已新增 `ours-rp-inter-rp-metrics` scrape job,默认访问 `host.docker.internal:9557`。
远端200 runner 与远端231同步脚本位于:
```text
scripts/inter_rp/run_remote200_rp_loops.sh
scripts/inter_rp/run_single_rp_with_rss.sh
scripts/inter_rp/sync_remote200_to_231.sh
scripts/inter_rp/run_inter_rp_metrics_sidecar.sh
scripts/inter_rp/inter-rp.env.example
```
如需从本机独立开关远端200上的 Routinator 或 rpki-client,使用:
```bash
scripts/inter_rp/control_remote200_rp.sh status all
scripts/inter_rp/control_remote200_rp.sh stop routinator
scripts/inter_rp/control_remote200_rp.sh start routinator
scripts/inter_rp/control_remote200_rp.sh restart rpki-client
```
默认远端为 `root@43.110.128.200`,可通过 `REMOTE_HOST=...` 覆盖;脚本只管理指定 RP 的 loop 和当前子进程,不会自动影响另一个 RP。
关键指标:
- `inter_rp_run_wall_seconds{rp="ours-rp|routinator|rpki-client"}`
- `inter_rp_run_max_rss_bytes{rp="...",kind="aggregate_peak"}`
- `inter_rp_vrps{rp="..."}`:按 `(ASN, IP Prefix, Max Length)` 去重。
- `inter_rp_vaps{rp="..."}`:按 `(Customer ASN, Providers)` 去重,Routinator 使用 `--enable-aspa` JSON 输出转换,rpki-client 使用 `-j` JSON 输出转换。
- `inter_rp_ccr_digest_match{left="ours-rp",right="rpki-client",state="overall|mfts|vrps|vaps|tas|rks"}`
- `inter_rp_sync_age_seconds`
Grafana dashboard:
- <http://localhost:3000/d/ours-rp-inter-rp/ours-rp-inter-rp>

View File

@@ -0,0 +1,38 @@
services:
prometheus:
image: ${PROMETHEUS_IMAGE:-prom/prometheus:v2.55.1}
container_name: ours-rp-prometheus
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=${PROMETHEUS_RETENTION:-7d}
- --web.enable-lifecycle
extra_hosts:
- host.docker.internal:host-gateway
ports:
- "${PROMETHEUS_PORT:-9090}:9090"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
restart: unless-stopped
grafana:
image: ${GRAFANA_IMAGE:-grafana/grafana:11.3.1}
container_name: ours-rp-grafana
depends_on:
- prometheus
ports:
- "${GRAFANA_PORT:-3000}:3000"
environment:
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin}
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-admin}
GF_USERS_ALLOW_SIGN_UP: "false"
volumes:
- grafana-data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
restart: unless-stopped
volumes:
prometheus-data:
grafana-data:

View File

@@ -0,0 +1,826 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"id": 1,
"title": "Ours Only Repo Count",
"type": "stat",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 0,
"y": 0
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"expr": "max(inter_rp_repo_sync_overlap_total{exported_instance=~\".*inter-rp\",left=\"ours-rp\",right=\"routinator\",class=\"only_ours\"})",
"legendFormat": "only ours",
"refId": "A",
"instant": true
}
]
},
{
"id": 2,
"title": "Routinator Only Repo Count",
"type": "stat",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 6,
"y": 0
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"expr": "max(inter_rp_repo_sync_overlap_total{exported_instance=~\".*inter-rp\",left=\"ours-rp\",right=\"routinator\",class=\"only_routinator\"})",
"legendFormat": "only routinator",
"refId": "A",
"instant": true
}
]
},
{
"id": 3,
"title": "Ours vs Routinator VAP Diff",
"type": "stat",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 12,
"y": 0
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"expr": "max(inter_rp_vaps_diff{exported_instance=~\".*inter-rp\",left=\"ours-rp\",right=\"routinator\"})",
"legendFormat": "vap diff",
"refId": "A",
"instant": true
}
]
},
{
"id": 4,
"title": "Ours vs Routinator VRP Diff",
"type": "stat",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 18,
"y": 0
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"expr": "max(inter_rp_vrps_diff{exported_instance=~\".*inter-rp\",left=\"ours-rp\",right=\"routinator\"})",
"legendFormat": "vrp diff",
"refId": "A",
"instant": true
}
]
},
{
"id": 5,
"title": "Wall Time by RP",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 4
},
"fieldConfig": {
"defaults": {
"unit": "s",
"min": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "inter_rp_run_wall_seconds{exported_instance=~\".*inter-rp\",rp=~\"ours-rp|routinator\"}",
"legendFormat": "{{rp}}",
"refId": "A"
}
]
},
{
"id": 6,
"title": "Max RSS Aggregate Peak by RP",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 4
},
"fieldConfig": {
"defaults": {
"unit": "bytes",
"min": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "inter_rp_run_max_rss_bytes{exported_instance=~\".*inter-rp\",kind=\"aggregate_peak\",rp=~\"ours-rp|routinator\"}",
"legendFormat": "{{rp}}",
"refId": "A"
}
]
},
{
"id": 7,
"title": "VRPs by RP (unique ASN/Prefix/MaxLen)",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 12
},
"fieldConfig": {
"defaults": {
"unit": "none",
"min": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "inter_rp_vrps{exported_instance=~\".*inter-rp\",rp=~\"ours-rp|routinator\"}",
"legendFormat": "{{rp}}",
"refId": "A"
}
]
},
{
"id": 8,
"title": "VAPs / ASPAs by RP (unique Customer/Providers)",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 12
},
"fieldConfig": {
"defaults": {
"unit": "none",
"min": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "inter_rp_vaps{exported_instance=~\".*inter-rp\",rp=~\"ours-rp|routinator\"}",
"legendFormat": "{{rp}}",
"refId": "A"
}
]
},
{
"id": 9,
"title": "Latest RP Runs",
"type": "table",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 20
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0
},
"overrides": []
},
"options": {
"showHeader": true,
"sortBy": []
},
"targets": [
{
"expr": "inter_rp_run_seq{exported_instance=~\".*inter-rp\",rp=~\"ours-rp|routinator\"}",
"format": "table",
"instant": true,
"legendFormat": "{{rp}} seq",
"refId": "A"
},
{
"expr": "inter_rp_run_success{exported_instance=~\".*inter-rp\",rp=~\"ours-rp|routinator\"}",
"format": "table",
"instant": true,
"legendFormat": "{{rp}} success",
"refId": "B"
},
{
"expr": "inter_rp_run_wall_seconds{exported_instance=~\".*inter-rp\",rp=~\"ours-rp|routinator\"}",
"format": "table",
"instant": true,
"legendFormat": "{{rp}} wall",
"refId": "C"
}
]
},
{
"id": 10,
"title": "Output Count Diffs (unique)",
"type": "table",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 20
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0
},
"overrides": []
},
"options": {
"showHeader": true,
"sortBy": []
},
"targets": [
{
"expr": "inter_rp_vrps_diff{exported_instance=~\".*inter-rp\",left=\"ours-rp\",right=\"routinator\"}",
"format": "table",
"instant": true,
"legendFormat": "vrps ours-rp-routinator",
"refId": "A"
},
{
"expr": "inter_rp_vaps_diff{exported_instance=~\".*inter-rp\",left=\"ours-rp\",right=\"routinator\"}",
"format": "table",
"instant": true,
"legendFormat": "vaps ours-rp-routinator",
"refId": "B"
}
]
},
{
"id": 15,
"title": "VRP Diff Trend by Class",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 28
},
"fieldConfig": {
"defaults": {
"unit": "none",
"min": 0,
"decimals": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "inter_rp_vrps_diff_by_class{exported_instance=~\".*inter-rp\",left=\"ours-rp\",right=\"routinator\",class=\"total\"}",
"legendFormat": "total diff",
"refId": "A"
},
{
"expr": "inter_rp_vrps_diff_by_class{exported_instance=~\".*inter-rp\",left=\"ours-rp\",right=\"routinator\",class=\"only_ours\"}",
"legendFormat": "only ours",
"refId": "B"
},
{
"expr": "inter_rp_vrps_diff_by_class{exported_instance=~\".*inter-rp\",left=\"ours-rp\",right=\"routinator\",class=\"only_routinator\"}",
"legendFormat": "only routinator",
"refId": "C"
}
]
},
{
"id": 16,
"title": "VAP Diff Trend by Class",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 28
},
"fieldConfig": {
"defaults": {
"unit": "none",
"min": 0,
"decimals": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "inter_rp_vaps_diff_by_class{exported_instance=~\".*inter-rp\",left=\"ours-rp\",right=\"routinator\",class=\"total\"}",
"legendFormat": "total diff",
"refId": "A"
},
{
"expr": "inter_rp_vaps_diff_by_class{exported_instance=~\".*inter-rp\",left=\"ours-rp\",right=\"routinator\",class=\"only_ours\"}",
"legendFormat": "only ours",
"refId": "B"
},
{
"expr": "inter_rp_vaps_diff_by_class{exported_instance=~\".*inter-rp\",left=\"ours-rp\",right=\"routinator\",class=\"only_routinator\"}",
"legendFormat": "only routinator",
"refId": "C"
}
]
},
{
"id": 11,
"title": "Artifact Age by RP",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 36
},
"fieldConfig": {
"defaults": {
"unit": "s",
"min": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "inter_rp_artifact_age_seconds{exported_instance=~\".*inter-rp\",rp=~\"ours-rp|routinator\"}",
"legendFormat": "{{rp}}",
"refId": "A"
}
]
},
{
"id": 12,
"title": "Repo Sync Availability by RP",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 44
},
"fieldConfig": {
"defaults": {
"unit": "none",
"min": 0,
"decimals": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "inter_rp_repo_sync_total{exported_instance=~\".*inter-rp\",state=~\"available|failed\",rp=~\"ours-rp|routinator\"}",
"legendFormat": "{{rp}} {{state}}",
"refId": "A"
}
]
},
{
"id": 13,
"title": "Repo Sync Overlap Classes",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 44
},
"fieldConfig": {
"defaults": {
"unit": "none",
"min": 0,
"decimals": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "inter_rp_repo_sync_overlap_total{exported_instance=~\".*inter-rp\",left=\"ours-rp\",right=\"routinator\"}",
"legendFormat": "{{class}}",
"refId": "A"
}
]
},
{
"id": 14,
"title": "Repo Sync Diff URIs",
"type": "table",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 9,
"w": 24,
"x": 0,
"y": 52
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "uri"
},
"properties": [
{
"id": "custom.width",
"value": 760
}
]
},
{
"matcher": {
"id": "byName",
"options": "class"
},
"properties": [
{
"id": "custom.width",
"value": 140
}
]
},
{
"matcher": {
"id": "byRegexp",
"options": "^(mft|crl|crt|roa|aspa)$"
},
"properties": [
{
"id": "custom.align",
"value": "right"
},
{
"id": "custom.width",
"value": 80
}
]
}
]
},
"options": {
"showHeader": true,
"sortBy": []
},
"targets": [
{
"expr": "inter_rp_repo_sync_diff_info{exported_instance=~\".*inter-rp\",left=\"ours-rp\",right=\"routinator\"}",
"format": "table",
"instant": true,
"legendFormat": "{{class}} #{{rank}}",
"refId": "A"
}
],
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true,
"Value": true,
"__name__": true,
"exported_instance": true,
"instance": true,
"job": true,
"left": true,
"right": true,
"rank": true,
"routinator_duration": true
},
"indexByName": {
"class": 0,
"uri": 1,
"mft": 2,
"crl": 3,
"crt": 4,
"roa": 5,
"aspa": 6
},
"renameByName": {
"class": "class",
"uri": "uri",
"mft": "mft",
"crl": "crl",
"crt": "crt",
"roa": "roa",
"aspa": "aspa"
}
}
}
]
}
],
"refresh": "10s",
"schemaVersion": 40,
"tags": [
"rpki",
"inter-rp",
"routinator"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timezone": "browser",
"title": "Ours RP vs Routinator",
"uid": "ours-rp-inter-rp",
"version": 4
}

View File

@@ -0,0 +1,761 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 6,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "ours_rp_publication_points",
"legendFormat": "publication points",
"refId": "A"
}
],
"title": "Publication Points",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 6,
"x": 6,
"y": 0
},
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "ours_rp_repo_sync_phase_count{phase=\"rrdp_ok\"}",
"legendFormat": "rrdp ok",
"refId": "A"
}
],
"title": "RRDP OK Points",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 6,
"x": 12,
"y": 0
},
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "sum(ours_rp_repo_sync_phase_count{phase=\"rrdp_failed_rsync_ok\"}) or vector(0)",
"legendFormat": "fallback",
"refId": "A"
}
],
"title": "Rsync Fallback Points",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 6,
"x": 18,
"y": 0
},
"id": 4,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "ours_rp_repo_terminal_state_count{terminal_state=\"failed_no_cache\"}",
"legendFormat": "failed no cache",
"refId": "A"
}
],
"title": "Failed No Cache Points",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 4
},
"id": 5,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_run_stage_duration_seconds{stage=\"repo_sync_total\"}",
"legendFormat": "repo sync total",
"refId": "A"
},
{
"expr": "ours_rp_run_stage_duration_seconds{stage=\"rrdp_download_total\"}",
"legendFormat": "rrdp download",
"refId": "B"
},
{
"expr": "ours_rp_run_stage_duration_seconds{stage=\"rsync_download_total\"}",
"legendFormat": "rsync download",
"refId": "C"
}
],
"title": "Repo Sync Download Durations",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 12,
"w": 12,
"h": 8
},
"id": 6,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_repo_sync_phase_count",
"legendFormat": "{{phase}}",
"refId": "A"
}
],
"title": "Repo Sync Phase Counts",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"gridPos": {
"x": 12,
"y": 12,
"w": 12,
"h": 8
},
"id": 7,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_repo_sync_phase_count{phase=\"rrdp_failed_rsync_ok\"}",
"legendFormat": "rrdp failed, rsync ok",
"refId": "A"
},
{
"expr": "ours_rp_repo_sync_phase_count{phase=\"rrdp_failed_rsync_failed\"}",
"legendFormat": "rrdp failed, rsync failed",
"refId": "B"
},
{
"expr": "ours_rp_repo_terminal_state_count{terminal_state=\"failed_no_cache\"}",
"legendFormat": "failed no cache",
"refId": "C"
},
{
"expr": "ours_rp_tree_instances{state=\"failed\"}",
"legendFormat": "tree failed",
"refId": "D"
}
],
"title": "Repo Failure / Fallback Counts",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "s"
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 20,
"w": 12,
"h": 8
},
"id": 8,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_repo_sync_phase_duration_seconds_total{phase=\"rrdp_failed_rsync_ok\"}",
"legendFormat": "rsync fallback duration",
"refId": "A"
},
{
"expr": "ours_rp_repo_sync_phase_duration_seconds_total{phase=\"rrdp_failed_rsync_failed\"}",
"legendFormat": "failed duration",
"refId": "B"
},
{
"expr": "ours_rp_repo_terminal_state_duration_seconds_total{terminal_state=\"failed_no_cache\"}",
"legendFormat": "failed no cache duration",
"refId": "C"
}
],
"title": "Repo Failure / Fallback Durations",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "none"
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 29,
"w": 12,
"h": 9
},
"id": 9,
"options": {
"showHeader": true,
"cellHeight": "sm",
"footer": {
"show": false,
"reducer": [
"sum"
],
"countRows": false,
"fields": ""
}
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "ours_rp_rrdp_rsync_failed_repository_duration_seconds",
"format": "table",
"instant": true,
"legendFormat": "",
"refId": "A"
}
],
"title": "RRDP + Rsync Failed Repositories",
"type": "table",
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"job": true,
"terminal_state": true,
"rank": true,
"transport": true,
"__name__": true,
"publication_points": true,
"instance": true,
"repo_id": true,
"pp_id": true,
"exported_instance": true,
"rp": true,
"source": true
},
"indexByName": {
"Time": 0,
"host": 1,
"phase": 2,
"uri": 3,
"Value": 4
},
"renameByName": {
"Value": "duration"
}
}
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "none"
},
"overrides": []
},
"gridPos": {
"x": 12,
"y": 29,
"w": 12,
"h": 9
},
"id": 11,
"options": {
"showHeader": true,
"cellHeight": "sm",
"footer": {
"show": false,
"reducer": [
"sum"
],
"countRows": false,
"fields": ""
}
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "topk(20, ours_rp_top_repository_sync_duration_seconds)",
"format": "table",
"instant": true,
"legendFormat": "",
"refId": "A"
}
],
"title": "Top 20 Repositories by Sync Duration",
"type": "table",
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"job": true,
"terminal_state": true,
"__name__": true,
"publication_points": true,
"instance": true,
"repo_id": true,
"phase": true,
"pp_id": true,
"exported_instance": true,
"rp": true,
"source": true
},
"indexByName": {
"Time": 0,
"host": 1,
"rank": 2,
"transport": 3,
"uri": 4,
"Value": 5
},
"renameByName": {
"Value": "value"
}
}
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "none"
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 38,
"w": 24,
"h": 9
},
"id": 10,
"options": {
"showHeader": true,
"cellHeight": "sm",
"footer": {
"show": false,
"reducer": [
"sum"
],
"countRows": false,
"fields": ""
}
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "topk(20, ours_rp_top_publication_point_object_count)",
"format": "table",
"instant": true,
"legendFormat": "",
"refId": "A"
}
],
"title": "Top Publication Points by Objects",
"type": "table",
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"job": true,
"__name__": true,
"publication_points": true,
"instance": true,
"repo_id": true,
"phase": true,
"pp_id": true,
"exported_instance": true,
"rp": true,
"source": true
},
"indexByName": {
"Time": 0,
"host": 1,
"rank": 2,
"terminal_state": 3,
"transport": 4,
"uri": 5,
"Value": 6
},
"renameByName": {}
}
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"description": "Per-repository sync success in the latest successful run; 1 means successful, 0 means failed or failed_no_cache.",
"fieldConfig": {
"defaults": {
"unit": "bool"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 47
},
"id": 12,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"expr": "ours_rp_repository_sync_success",
"legendFormat": "{{host}} {{repo_id}}",
"refId": "A"
}
],
"title": "Repository Sync Success by Repo",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"description": "Per-repository total sync duration aggregated from publication point repo_sync_duration_ms.",
"fieldConfig": {
"defaults": {
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 55
},
"id": 13,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"expr": "ours_rp_repository_sync_duration_seconds{stat=\"sum\"}",
"legendFormat": "{{host}} {{repo_id}}",
"refId": "A"
}
],
"title": "Repository Sync Duration by Repo",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"description": "Per-repository downloaded bytes attributed from report.json downloads events.",
"fieldConfig": {
"defaults": {
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 63
},
"id": 14,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"expr": "ours_rp_repository_download_bytes",
"legendFormat": "{{host}} {{repo_id}}",
"refId": "A"
}
],
"title": "Repository Download Bytes by Repo",
"type": "timeseries"
}
],
"refresh": "5s",
"schemaVersion": 40,
"tags": [
"ours-rp",
"rpki",
"soak",
"repo-sync"
],
"templating": {
"list": []
},
"time": {
"from": "now-30m",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "Ours RP Repo Sync",
"uid": "ours-rp-repo-sync",
"version": 3,
"weekStart": ""
}

View File

@@ -0,0 +1,666 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"id": 1,
"title": "RTR Metrics Enabled",
"type": "stat",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 4,
"w": 4,
"x": 0,
"y": 0
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"expr": "max(ours_rp_rtr_metrics_enabled)",
"legendFormat": "enabled",
"refId": "A",
"instant": true
}
]
},
{
"id": 2,
"title": "Refresh Success",
"type": "stat",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 4,
"w": 5,
"x": 4,
"y": 0
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"expr": "max(ours_rp_rtr_source_refresh_status{status=\"success\"})",
"legendFormat": "success",
"refId": "A",
"instant": true
}
]
},
{
"id": 3,
"title": "Consecutive Failures",
"type": "stat",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 4,
"w": 5,
"x": 9,
"y": 0
},
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"expr": "max(ours_rp_rtr_source_refresh_consecutive_failures)",
"legendFormat": "failures",
"refId": "A",
"instant": true
}
]
},
{
"id": 4,
"title": "Last Success Age",
"type": "stat",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 4,
"w": 5,
"x": 14,
"y": 0
},
"fieldConfig": {
"defaults": {
"unit": "s",
"decimals": 0,
"min": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"expr": "max(ours_rp_rtr_source_last_success_age_seconds)",
"legendFormat": "age",
"refId": "A",
"instant": true
}
]
},
{
"id": 5,
"title": "RTR RSS",
"type": "stat",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 4,
"w": 5,
"x": 19,
"y": 0
},
"fieldConfig": {
"defaults": {
"unit": "bytes",
"decimals": 0,
"min": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"expr": "max(ours_rp_rtr_process_rss_bytes)",
"legendFormat": "rss",
"refId": "A",
"instant": true
}
]
},
{
"id": 6,
"title": "Data Quality Totals",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 4
},
"fieldConfig": {
"defaults": {
"unit": "short",
"min": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_rtr_data_quality_items{stage=~\"ccr_input|before_slurm|after_slurm\",type=\"total\"}",
"legendFormat": "{{stage}} total",
"refId": "A"
},
{
"expr": "ours_rp_rtr_data_quality_items{stage=\"after_slurm\",type=\"vrp\"}",
"legendFormat": "after_slurm vrp",
"refId": "B"
},
{
"expr": "ours_rp_rtr_data_quality_items{stage=\"after_slurm\",type=\"aspa\"}",
"legendFormat": "after_slurm aspa",
"refId": "C"
}
]
},
{
"id": 7,
"title": "SLURM Filters / Assertions",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 4
},
"fieldConfig": {
"defaults": {
"unit": "short",
"min": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_rtr_slurm_filters",
"legendFormat": "filter {{type}}",
"refId": "A"
},
{
"expr": "ours_rp_rtr_slurm_assertions",
"legendFormat": "assert {{type}}",
"refId": "B"
}
]
},
{
"id": 8,
"title": "Cache Ready / Delta Window",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 8,
"x": 0,
"y": 12
},
"fieldConfig": {
"defaults": {
"unit": "short",
"min": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_rtr_cache_ready",
"legendFormat": "ready",
"refId": "A"
},
{
"expr": "ours_rp_rtr_cache_delta_window_length",
"legendFormat": "v{{version}} length",
"refId": "B"
}
]
},
{
"id": 9,
"title": "Cache Snapshot Items",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 8,
"x": 8,
"y": 12
},
"fieldConfig": {
"defaults": {
"unit": "short",
"min": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_rtr_cache_snapshot_items",
"legendFormat": "v{{version}} {{type}}",
"refId": "A"
}
]
},
{
"id": 10,
"title": "Latest Delta Items",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 8,
"x": 16,
"y": 12
},
"fieldConfig": {
"defaults": {
"unit": "short",
"min": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_rtr_cache_delta_items",
"legendFormat": "v{{version}} {{direction}} {{type}}",
"refId": "A"
}
]
},
{
"id": 11,
"title": "Connections",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 8,
"x": 0,
"y": 20
},
"fieldConfig": {
"defaults": {
"unit": "short",
"min": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_rtr_active_connections",
"legendFormat": "active",
"refId": "A"
},
{
"expr": "ours_rp_rtr_connections",
"legendFormat": "{{transport}}",
"refId": "B"
}
]
},
{
"id": 12,
"title": "Connection Utilization / Max",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 8,
"x": 8,
"y": 20
},
"fieldConfig": {
"defaults": {
"min": 0
},
"overrides": [
{
"matcher": {
"id": "byRegexp",
"options": ".*utilization.*"
},
"properties": [
{
"id": "unit",
"value": "percentunit"
}
]
},
{
"matcher": {
"id": "byRegexp",
"options": ".*max.*"
},
"properties": [
{
"id": "unit",
"value": "short"
}
]
}
]
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_rtr_connection_utilization",
"legendFormat": "utilization",
"refId": "A"
},
{
"expr": "ours_rp_rtr_max_connections",
"legendFormat": "max",
"refId": "B"
}
]
},
{
"id": 13,
"title": "Report Age",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"gridPos": {
"h": 8,
"w": 8,
"x": 16,
"y": 20
},
"fieldConfig": {
"defaults": {
"unit": "s",
"min": 0
},
"overrides": []
},
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_rtr_source_report_age_seconds",
"legendFormat": "source",
"refId": "A"
},
{
"expr": "ours_rp_rtr_runtime_report_age_seconds",
"legendFormat": "runtime",
"refId": "B"
},
{
"expr": "ours_rp_rtr_clients_report_age_seconds",
"legendFormat": "clients",
"refId": "C"
}
]
}
],
"refresh": "10s",
"schemaVersion": 40,
"tags": [
"rpki",
"inter-rp",
"routinator"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timezone": "browser",
"title": "RTR Service Overview",
"uid": "ours-rp-rtr-overview",
"version": 1
}

View File

@@ -0,0 +1,875 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"unit": "none"
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 0,
"w": 6,
"h": 4
},
"id": 1,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "ours_rp_cir_trust_anchors",
"legendFormat": "RIRs",
"refId": "A"
}
],
"title": "Current Run RIRs",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "s"
},
"overrides": []
},
"gridPos": {
"x": 6,
"y": 0,
"w": 6,
"h": 4
},
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "ours_rp_run_duration_seconds",
"legendFormat": "wall",
"refId": "A"
}
],
"title": "Latest Wall Time",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"x": 12,
"y": 0,
"w": 6,
"h": 4
},
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "ours_rp_run_max_rss_bytes",
"legendFormat": "rss",
"refId": "A"
}
],
"title": "Latest Max RSS",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"unit": "none"
},
"overrides": []
},
"gridPos": {
"x": 18,
"y": 0,
"w": 6,
"h": 4
},
"id": 4,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "ours_rp_publication_points",
"legendFormat": "publication points",
"refId": "A"
}
],
"title": "Publication Points",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"unit": "none"
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 4,
"w": 6,
"h": 4
},
"id": 9,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "ours_rp_run_sequence",
"legendFormat": "seq",
"refId": "A"
}
],
"title": "Latest Run Sequence",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"decimals": 2,
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": null
},
{
"color": "orange",
"value": 90
},
{
"color": "green",
"value": 98
}
]
}
},
"overrides": []
},
"gridPos": {
"x": 6,
"y": 4,
"w": 6,
"h": 4
},
"id": 10,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "100 * sum by (job, instance, exported_instance) (ours_rp_repo_terminal_state_count{terminal_state=\"publication_point_cache\"}) / sum by (job, instance, exported_instance) (ours_rp_publication_points)",
"legendFormat": "PP cache hit ratio",
"refId": "A"
}
],
"title": "Latest PP Cache Hit Ratio",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0
},
"overrides": []
},
"gridPos": {
"x": 12,
"y": 4,
"w": 6,
"h": 4
},
"id": 11,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "ours_rp_vrps{kind=\"total\"}",
"legendFormat": "VRPs raw",
"refId": "A"
}
],
"title": "VRPs",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0
},
"overrides": []
},
"gridPos": {
"x": 18,
"y": 4,
"w": 6,
"h": 4
},
"id": 12,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "11.3.1",
"targets": [
{
"expr": "ours_rp_vaps",
"legendFormat": "VAPs",
"refId": "A"
}
],
"title": "VAPs",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "s"
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 8,
"w": 12,
"h": 8
},
"id": 5,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_run_duration_seconds",
"legendFormat": "wall",
"refId": "A"
},
{
"expr": "ours_rp_stage_duration_seconds{stage=\"validation\"}",
"legendFormat": "validation",
"refId": "B"
}
],
"title": "Run / Validation Duration",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"unit": "none"
},
"overrides": []
},
"gridPos": {
"x": 12,
"y": 8,
"w": 12,
"h": 8
},
"id": 6,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_vrps{kind=\"total\"}",
"legendFormat": "VRPs raw",
"refId": "A"
},
{
"expr": "ours_rp_vrps{kind=\"unique\"}",
"legendFormat": "VRPs unique",
"refId": "D"
},
{
"expr": "ours_rp_vaps",
"legendFormat": "VAPs",
"refId": "B"
},
{
"expr": "ours_rp_cir_objects",
"legendFormat": "CIR objects",
"refId": "C"
}
],
"title": "Output and Input Sizes",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"decimals": 0,
"unit": "none"
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 16,
"w": 12,
"h": 8
},
"id": 8,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_large_publication_points",
"legendFormat": "> {{object_count_gt}} objects",
"refId": "A"
}
],
"title": "Large Publication Points by Object Count",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "s"
},
"overrides": []
},
"gridPos": {
"x": 12,
"y": 16,
"w": 12,
"h": 8
},
"id": 13,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_run_stage_duration_seconds{stage=\"validation\"}",
"legendFormat": "validation",
"refId": "A"
},
{
"expr": "ours_rp_run_stage_duration_seconds{stage=\"report_write\"}",
"legendFormat": "report write",
"refId": "E"
},
{
"expr": "ours_rp_run_stage_duration_seconds{stage=\"ccr_write\"}",
"legendFormat": "ccr write",
"refId": "F"
},
{
"expr": "ours_rp_run_stage_duration_seconds{stage=\"cir_write\"}",
"legendFormat": "cir write",
"refId": "G"
}
],
"title": "Output Stage Durations",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "bytes",
"decimals": 2
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 24,
"w": 12,
"h": 8
},
"id": 14,
"options": {
"legend": {
"calcs": [
"lastNotNull",
"max"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_run_max_rss_bytes",
"legendFormat": "Max RSS",
"refId": "A"
}
],
"title": "Max RSS Over Time",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "percent",
"decimals": 2,
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": null
},
{
"color": "orange",
"value": 90
},
{
"color": "green",
"value": 98
}
]
}
},
"overrides": []
},
"gridPos": {
"x": 12,
"y": 24,
"w": 12,
"h": 8
},
"id": 17,
"options": {
"legend": {
"calcs": [
"lastNotNull",
"min",
"max"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "100 * sum by (job, instance, exported_instance) (ours_rp_repo_terminal_state_count{terminal_state=\"publication_point_cache\"}) / sum by (job, instance, exported_instance) (ours_rp_publication_points)",
"legendFormat": "PP cache hit ratio",
"refId": "A"
}
],
"title": "PP Cache Hit Ratio",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "bytes",
"decimals": 2
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 32,
"w": 24,
"h": 8
},
"id": 15,
"options": {
"legend": {
"calcs": [
"lastNotNull",
"max"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_state_db_size_bytes",
"legendFormat": "{{db}}",
"refId": "A"
}
],
"title": "State DB Size Over Time",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 40,
"w": 24,
"h": 8
},
"id": 16,
"options": {
"legend": {
"calcs": [
"lastNotNull",
"max"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "ours_rp_state_db_files",
"legendFormat": "{{db}}",
"refId": "A"
}
],
"title": "State DB File Count Over Time",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "Prometheus"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0,
"min": 0
},
"overrides": []
},
"gridPos": {
"x": 0,
"y": 48,
"w": 24,
"h": 8
},
"id": 18,
"options": {
"legend": {
"calcs": [
"lastNotNull",
"max"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "sum by (job, instance, exported_instance) (ours_rp_repo_terminal_state_count{terminal_state=\"fresh\"})",
"legendFormat": "fresh pp",
"refId": "A"
},
{
"expr": "sum by (job, instance, exported_instance) (ours_rp_cir_objects_by_source_type{exported_source=\"fresh\",object_type=\"roa\"})",
"legendFormat": "fresh roa",
"refId": "B"
},
{
"expr": "sum by (job, instance, exported_instance) (ours_rp_cir_objects_by_source_type{exported_source=\"fresh\",object_type=\"manifest\"})",
"legendFormat": "fresh mft",
"refId": "C"
},
{
"expr": "sum by (job, instance, exported_instance) (ours_rp_cir_objects_by_source_type{exported_source=\"fresh\",object_type=\"certificate\"})",
"legendFormat": "fresh crt",
"refId": "D"
},
{
"expr": "sum by (job, instance, exported_instance) (ours_rp_cir_objects_by_source_type{exported_source=\"fresh\",object_type=\"crl\"})",
"legendFormat": "fresh crl",
"refId": "E"
}
],
"title": "Fresh PP / Object Counts by Run",
"type": "timeseries"
}
],
"refresh": "5s",
"schemaVersion": 40,
"tags": [
"ours-rp",
"rpki",
"soak"
],
"templating": {
"list": []
},
"time": {
"from": "now-30m",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "Ours RP Soak Overview",
"uid": "ours-rp-soak-overview",
"version": 4,
"weekStart": ""
}

View File

@@ -0,0 +1,12 @@
apiVersion: 1
providers:
- name: ours-rp
orgId: 1
folder: Ours RP
type: file
disableDeletion: false
updateIntervalSeconds: 10
allowUiUpdates: true
options:
path: /var/lib/grafana/dashboards

View File

@@ -0,0 +1,10 @@
apiVersion: 1
datasources:
- name: Prometheus
uid: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true

View File

@@ -0,0 +1,20 @@
global:
scrape_interval: 5s
evaluation_interval: 5s
scrape_configs:
- job_name: ours-rp-artifact-metrics
metrics_path: /metrics
static_configs:
- targets:
- host.docker.internal:9556
labels:
rp: ours-rp
source: artifact-sidecar
- job_name: ours-rp-inter-rp-metrics
metrics_path: /metrics
static_configs:
- targets:
- host.docker.internal:9557
labels:
source: inter-rp-sidecar

View File

@@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
check_rpki_client_ccr.sh \
--rpki-client <path> \
--ccr-root <directory-or-ccr-file> \
--out <result.jsonl> \
[--limit <count>]
Runs the supplied rpki-client importer against CCR files. A non-zero result
means that rpki-client reported a CCR-file parsing error or could not start.
Per-file command output is retained next to the JSONL result file.
EOF
}
RPKI_CLIENT=""
CCR_ROOT=""
OUT=""
LIMIT=0
while [[ $# -gt 0 ]]; do
case "$1" in
--rpki-client) RPKI_CLIENT=${2:?missing value for --rpki-client}; shift 2 ;;
--ccr-root) CCR_ROOT=${2:?missing value for --ccr-root}; shift 2 ;;
--out) OUT=${2:?missing value for --out}; shift 2 ;;
--limit) LIMIT=${2:?missing value for --limit}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "error: unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
done
[[ -n "$RPKI_CLIENT" && -n "$CCR_ROOT" && -n "$OUT" ]] || {
echo "error: --rpki-client, --ccr-root, and --out are required" >&2
usage >&2
exit 2
}
[[ -x "$RPKI_CLIENT" ]] || { echo "error: rpki-client is not executable: $RPKI_CLIENT" >&2; exit 2; }
[[ "$LIMIT" =~ ^[0-9]+$ ]] || { echo "error: --limit must be a non-negative integer" >&2; exit 2; }
mkdir -p "$(dirname "$OUT")"
LOG_DIR="${OUT%.jsonl}.logs"
mkdir -p "$LOG_DIR"
: > "$OUT"
declare -a CCR_FILES=()
if [[ -f "$CCR_ROOT" ]]; then
CCR_FILES=("$(readlink -f "$CCR_ROOT")")
elif [[ -d "$CCR_ROOT" ]]; then
while IFS= read -r -d '' file; do
CCR_FILES+=("$file")
done < <(find "$CCR_ROOT" -type f -name '*.ccr' -print0 | sort -z)
else
echo "error: CCR root does not exist: $CCR_ROOT" >&2
exit 2
fi
if (( LIMIT > 0 && ${#CCR_FILES[@]} > LIMIT )); then
CCR_FILES=("${CCR_FILES[@]:0:LIMIT}")
fi
if (( ${#CCR_FILES[@]} == 0 )); then
echo "error: no .ccr files found under: $CCR_ROOT" >&2
exit 2
fi
passed=0
failed=0
for ccr in "${CCR_FILES[@]}"; do
digest=$(sha256sum "$ccr" | awk '{print $1}')
log="$LOG_DIR/${digest}.log"
set +e
"$RPKI_CLIENT" -f "$ccr" >"$log" 2>&1
exit_code=$?
set -e
classification=pass
if (( exit_code != 0 )); then
classification=tool_error
elif grep -Fq "rpki-client: ${ccr}:" "$log"; then
classification=ccr_parse_error
fi
if [[ "$classification" == pass ]]; then
((passed += 1))
else
((failed += 1))
fi
python3 - "$OUT" "$ccr" "$digest" "$exit_code" "$classification" "$log" <<'PY'
import json
import sys
out, ccr, sha256, exit_code, classification, log = sys.argv[1:]
with open(out, "a", encoding="utf-8") as stream:
stream.write(json.dumps({
"ccr": ccr,
"sha256": sha256,
"rpkiClientExitCode": int(exit_code),
"classification": classification,
"log": log,
}, ensure_ascii=False, sort_keys=True) + "\n")
PY
done
echo "checked=${#CCR_FILES[@]} passed=${passed} failed=${failed} jsonl=${OUT} logs=${LOG_DIR}"
(( failed == 0 ))

View File

@@ -34,11 +34,7 @@ from pathlib import Path
sequence_root = Path(sys.argv[1]).resolve()
drop_bin = sys.argv[2]
sequence = json.loads((sequence_root / "sequence.json").read_text(encoding="utf-8"))
static_root = sequence_root / sequence["staticRoot"] if "staticRoot" in sequence else None
raw_store_db = sequence_root / sequence["rawStoreDbPath"] if "rawStoreDbPath" in sequence else None
backend_count = sum(x is not None for x in (static_root, raw_store_db))
if backend_count != 1:
raise SystemExit("sequence must set exactly one of staticRoot or rawStoreDbPath")
repo_bytes_db = sequence_root / sequence["repoBytesDbPath"]
summaries = []
for step in sequence["steps"]:
@@ -58,10 +54,7 @@ for step in sequence["steps"]:
"--md-out",
str(out_dir / "drop.md"),
]
if static_root is not None:
cmd.extend(["--static-root", str(static_root)])
else:
cmd.extend(["--raw-store-db", str(raw_store_db)])
cmd.extend(["--repo-bytes-db", str(repo_bytes_db)])
proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode != 0:
raise SystemExit(

View File

@@ -92,7 +92,8 @@ if [[ -z "$DELTA_VALIDATION_TIME" ]]; then
fi
rm -rf "$OUT_DIR"
mkdir -p "$OUT_DIR/full" "$OUT_DIR/delta-001" "$OUT_DIR/static"
mkdir -p "$OUT_DIR/full" "$OUT_DIR/delta-001"
REPO_BYTES_DB="$OUT_DIR/repo-bytes.db"
FULL_DB="$OUT_DIR/full/db"
DELTA_DB="$OUT_DIR/delta-001/db"
@@ -110,7 +111,7 @@ DELTA_DB="$OUT_DIR/delta-001/db"
--report-json "$OUT_DIR/full/report.json" \
--cir-enable \
--cir-out "$OUT_DIR/full/input.cir" \
--cir-static-root "$OUT_DIR/static" \
--repo-bytes-db "$REPO_BYTES_DB" \
--cir-tal-uri "$CIR_TAL_URI" \
>"$OUT_DIR/full/run.stdout.log" 2>"$OUT_DIR/full/run.stderr.log"
@@ -130,7 +131,7 @@ DELTA_DB="$OUT_DIR/delta-001/db"
--report-json "$OUT_DIR/delta-001/report.json" \
--cir-enable \
--cir-out "$OUT_DIR/delta-001/input.cir" \
--cir-static-root "$OUT_DIR/static" \
--repo-bytes-db "$REPO_BYTES_DB" \
--cir-tal-uri "$CIR_TAL_URI" \
>"$OUT_DIR/delta-001/run.stdout.log" 2>"$OUT_DIR/delta-001/run.stderr.log"
@@ -143,13 +144,12 @@ from pathlib import Path
out = Path(sys.argv[1])
base_validation_time = sys.argv[2]
delta_validation_time = sys.argv[3]
static_files = sum(1 for _ in (out / "static").rglob("*") if _.is_file())
summary = {
"version": 1,
"kind": "cir_pair",
"baseValidationTime": base_validation_time,
"deltaValidationTime": delta_validation_time,
"staticRoot": "static",
"repoBytesDbPath": "repo-bytes.db",
"steps": [
{
"kind": "full",
@@ -165,7 +165,7 @@ summary = {
"previous": "full",
},
],
"staticFileCount": static_files,
"repoBytesDbExists": (out / "repo-bytes.db").exists(),
}
(out / "summary.json").write_text(json.dumps(summary, indent=2), encoding="utf-8")
PY

View File

@@ -116,13 +116,13 @@ for rir in rirs:
"rir": rir,
"root": str(root),
"stepCount": len(seq["steps"]),
"staticFileCount": summ["staticFileCount"],
"repoBytesDbExists": summ.get("repoBytesDbExists", False),
})
summary = {"version": 1, "rirs": items}
summary_json.write_text(json.dumps(summary, indent=2), encoding="utf-8")
lines = ["# Multi-RIR Offline CIR Sequence Summary", ""]
for item in items:
lines.append(f"- `{item['rir']}`: `stepCount={item['stepCount']}` `staticFileCount={item['staticFileCount']}` `root={item['root']}`")
lines.append(f"- `{item['rir']}`: `stepCount={item['stepCount']}` `repoBytesDbExists={item['repoBytesDbExists']}` `root={item['root']}`")
summary_md.write_text("\n".join(lines) + "\n", encoding="utf-8")
PY

View File

@@ -97,7 +97,8 @@ if [[ -z "$DELTA_VALIDATION_TIME" ]]; then
fi
rm -rf "$OUT_DIR"
mkdir -p "$OUT_DIR/static" "$OUT_DIR/full"
mkdir -p "$OUT_DIR/full"
REPO_BYTES_DB="$OUT_DIR/repo-bytes.db"
run_step() {
local kind="$1"
@@ -114,7 +115,7 @@ run_step() {
--report-json "$step_dir/report.json" \
--cir-enable \
--cir-out "$step_dir/input.cir" \
--cir-static-root "$OUT_DIR/static" \
--repo-bytes-db "$REPO_BYTES_DB" \
--cir-tal-uri "$CIR_TAL_URI"
)
if [[ "$FULL_REPO" -ne 1 ]]; then
@@ -186,7 +187,7 @@ for idx in range(1, delta_count + 1):
summary = {
"version": 1,
"kind": "cir_sequence_offline",
"staticRoot": "static",
"repoBytesDbPath": "repo-bytes.db",
"steps": steps,
}
(out / "sequence.json").write_text(json.dumps(summary, indent=2), encoding="utf-8")
@@ -195,7 +196,8 @@ summary = {
{
"version": 1,
"stepCount": len(steps),
"staticFileCount": sum(1 for p in (out / "static").rglob("*") if p.is_file()),
"repoBytesDbPath": "repo-bytes.db",
"repoBytesDbExists": (out / "repo-bytes.db").exists(),
},
indent=2,
),

View File

@@ -87,6 +87,7 @@ fi
mkdir -p "$OUT"
DB="$OUT/work-db"
RAW_STORE_DB="$OUT/raw-store.db"
REPO_BYTES_DB="$OUT/repo-bytes.db"
ROWS="$OUT/.sequence_rows.tsv"
: > "$ROWS"
@@ -140,6 +141,7 @@ PY
target/release/rpki
--db "$DB"
--raw-store-db "$RAW_STORE_DB"
--repo-bytes-db "$REPO_BYTES_DB"
--tal-path "$TAL_REL"
--ta-path "$TA_REL"
--ccr-out "$ccr_out"
@@ -216,7 +218,7 @@ for idx, row in enumerate(rows):
})
(out / "sequence.json").write_text(
json.dumps({"version": 1, "rawStoreDbPath": "raw-store.db", "steps": steps}, indent=2),
json.dumps({"version": 1, "repoBytesDbPath": "repo-bytes.db", "steps": steps}, indent=2),
encoding="utf-8",
)

View File

@@ -56,16 +56,17 @@ IFS=',' read -r -a ITEMS <<< "$RIRS"
for rir in "${ITEMS[@]}"; do
read -r tal_rel ta_rel < <(case_paths "$rir")
rir_root="$OUT_ROOT/$rir"
mkdir -p "$rir_root/full" "$rir_root/static"
mkdir -p "$rir_root/full"
repo_bytes_db="$rir_root/repo-bytes.db"
"$HELPER_BIN" \
--tal-path "$ROOT_DIR/$tal_rel" \
--ta-path "$ROOT_DIR/$ta_rel" \
--tal-uri "https://example.test/$rir.tal" \
--validation-time "2026-04-09T00:00:00Z" \
--cir-out "$rir_root/full/input.cir" \
--static-root "$rir_root/static"
--repo-bytes-db "$repo_bytes_db"
"$EXTRACT_BIN" --cir "$rir_root/full/input.cir" --tals-dir "$rir_root/.tmp/tals" --meta-json "$rir_root/.tmp/meta.json"
"$MATERIALIZE_BIN" --cir "$rir_root/full/input.cir" --static-root "$rir_root/static" --mirror-root "$rir_root/.tmp/mirror"
"$MATERIALIZE_BIN" --cir "$rir_root/full/input.cir" --repo-bytes-db "$repo_bytes_db" --mirror-root "$rir_root/.tmp/mirror"
FIRST_TAL="$(python3 - <<'PY' "$rir_root/.tmp/meta.json"
import json,sys
print(json.load(open(sys.argv[1]))["talFiles"][0]["path"])
@@ -99,7 +100,7 @@ for i in range(1, delta_count + 1):
step = f"delta-{i:03d}"
steps.append({"stepId":step,"kind":"delta","validationTime":"2026-04-09T00:00:00Z","cirPath":f"{step}/input.cir","ccrPath":f"{step}/result.ccr","reportPath":f"{step}/report.json","previousStepId":prev})
prev = step
(root/"sequence.json").write_text(json.dumps({"version":1,"staticRoot":"static","steps":steps}, indent=2), encoding="utf-8")
(root/"sequence.json").write_text(json.dumps({"version":1,"repoBytesDbPath":"repo-bytes.db","steps":steps}, indent=2), encoding="utf-8")
(root/"summary.json").write_text(json.dumps({"version":1,"stepCount":len(steps)}, indent=2), encoding="utf-8")
PY
done

View File

@@ -6,7 +6,7 @@ usage() {
Usage:
./scripts/cir/run_cir_replay_matrix.sh \
--cir <path> \
--static-root <path> \
--repo-bytes-db <path> \
--out-dir <path> \
--reference-ccr <path> \
--rpki-client-build-dir <path> \
@@ -21,7 +21,7 @@ EOF
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
CIR=""
STATIC_ROOT=""
REPO_BYTES_DB=""
OUT_DIR=""
REFERENCE_CCR=""
RPKI_CLIENT_BUILD_DIR=""
@@ -37,7 +37,7 @@ RPKI_CLIENT_SCRIPT="$ROOT_DIR/scripts/cir/run_cir_replay_rpki_client.sh"
while [[ $# -gt 0 ]]; do
case "$1" in
--cir) CIR="$2"; shift 2 ;;
--static-root) STATIC_ROOT="$2"; shift 2 ;;
--repo-bytes-db) REPO_BYTES_DB="$2"; shift 2 ;;
--out-dir) OUT_DIR="$2"; shift 2 ;;
--reference-ccr) REFERENCE_CCR="$2"; shift 2 ;;
--rpki-client-build-dir) RPKI_CLIENT_BUILD_DIR="$2"; shift 2 ;;
@@ -51,7 +51,7 @@ while [[ $# -gt 0 ]]; do
esac
done
[[ -n "$CIR" && -n "$STATIC_ROOT" && -n "$OUT_DIR" && -n "$REFERENCE_CCR" && -n "$RPKI_CLIENT_BUILD_DIR" ]] || {
[[ -n "$CIR" && -n "$REPO_BYTES_DB" && -n "$OUT_DIR" && -n "$REFERENCE_CCR" && -n "$RPKI_CLIENT_BUILD_DIR" ]] || {
usage >&2
exit 2
}
@@ -102,7 +102,7 @@ mkdir -p "$OURS_OUT" "$ROUTINATOR_OUT" "$RPKI_CLIENT_OUT"
ours_cmd=(
"$OURS_SCRIPT"
--cir "$CIR"
--static-root "$STATIC_ROOT"
--repo-bytes-db "$REPO_BYTES_DB"
--out-dir "$OURS_OUT"
--reference-ccr "$REFERENCE_CCR"
--rpki-bin "$RPKI_BIN"
@@ -111,7 +111,7 @@ ours_cmd=(
routinator_cmd=(
"$ROUTINATOR_SCRIPT"
--cir "$CIR"
--static-root "$STATIC_ROOT"
--repo-bytes-db "$REPO_BYTES_DB"
--out-dir "$ROUTINATOR_OUT"
--reference-ccr "$REFERENCE_CCR"
--routinator-root "$ROUTINATOR_ROOT"
@@ -121,7 +121,7 @@ routinator_cmd=(
rpki_client_cmd=(
"$RPKI_CLIENT_SCRIPT"
--cir "$CIR"
--static-root "$STATIC_ROOT"
--repo-bytes-db "$REPO_BYTES_DB"
--out-dir "$RPKI_CLIENT_OUT"
--reference-ccr "$REFERENCE_CCR"
--build-dir "$RPKI_CLIENT_BUILD_DIR"
@@ -159,7 +159,7 @@ DETAIL_MD="$OUT_DIR/detail.md"
python3 - <<'PY' \
"$CIR" \
"$STATIC_ROOT" \
"$REPO_BYTES_DB" \
"$REFERENCE_CCR" \
"$OURS_OUT" \
"$ROUTINATOR_OUT" \
@@ -171,7 +171,7 @@ import json
import sys
from pathlib import Path
cir_path, static_root, reference_ccr, ours_out, routinator_out, rpki_client_out, summary_json, summary_md, detail_md = sys.argv[1:]
cir_path, repo_bytes_db, reference_ccr, ours_out, routinator_out, rpki_client_out, summary_json, summary_md, detail_md = sys.argv[1:]
participants = []
all_match = True
@@ -194,6 +194,9 @@ for name, out_dir in [
"summaryPath": str(out / "compare-summary.json"),
"exitCode": timing["exitCode"],
"durationMs": timing["durationMs"],
"compareMode": compare.get("compareMode"),
"talCount": compare.get("talCount"),
"talPaths": compare.get("talPaths", []),
"vrps": vrps,
"vaps": vaps,
"match": bool(vrps.get("match")) and bool(vaps.get("match")) and timing["exitCode"] == 0,
@@ -204,7 +207,7 @@ for name, out_dir in [
summary = {
"cirPath": cir_path,
"staticRoot": static_root,
"repoBytesDb": repo_bytes_db,
"referenceCcr": reference_ccr,
"participants": participants,
"allMatch": all_match,
@@ -215,22 +218,24 @@ lines = [
"# CIR Replay Matrix Summary",
"",
f"- `cir`: `{cir_path}`",
f"- `static_root`: `{static_root}`",
f"- `repo_bytes_db`: `{repo_bytes_db}`",
f"- `reference_ccr`: `{reference_ccr}`",
f"- `all_match`: `{all_match}`",
"",
"| Participant | Exit | Duration (ms) | VRP actual/ref | VRP match | VAP actual/ref | VAP match | Log |",
"| --- | ---: | ---: | --- | --- | --- | --- | --- |",
"| Participant | Exit | Duration (ms) | TALs | Compare mode | VRP actual/ref | VRP match | VAP actual/ref | VAP match | Log |",
"| --- | ---: | ---: | ---: | --- | --- | --- | --- | --- | --- |",
]
for participant in participants:
vrps = participant["vrps"] or {}
vaps = participant["vaps"] or {}
log_path = participant["logPaths"][0] if participant["logPaths"] else ""
lines.append(
"| {name} | {exit_code} | {duration_ms} | {vrp_actual}/{vrp_ref} | {vrp_match} | {vap_actual}/{vap_ref} | {vap_match} | `{log_path}` |".format(
"| {name} | {exit_code} | {duration_ms} | {tal_count} | {compare_mode} | {vrp_actual}/{vrp_ref} | {vrp_match} | {vap_actual}/{vap_ref} | {vap_match} | `{log_path}` |".format(
name=participant["name"],
exit_code=participant["exitCode"],
duration_ms=participant["durationMs"],
tal_count=participant.get("talCount") if participant.get("talCount") is not None else "-",
compare_mode=participant.get("compareMode") or "-",
vrp_actual=vrps.get("actual", "-"),
vrp_ref=vrps.get("reference", "-"),
vrp_match=vrps.get("match", False),
@@ -258,6 +263,8 @@ for participant in participants:
f"- `mirror_path`: `{participant['mirrorPath']}`",
f"- `summary_path`: `{participant['summaryPath']}`",
f"- `timing_path`: `{participant['timingPath']}`",
f"- `compare_mode`: `{participant.get('compareMode')}`",
f"- `tal_count`: `{participant.get('talCount')}`",
f"- `log_paths`: `{', '.join(participant['logPaths'])}`",
f"- `vrps`: `actual={vrps.get('actual', '-')}` `reference={vrps.get('reference', '-')}` `match={vrps.get('match', False)}`",
f"- `vaps`: `actual={vaps.get('actual', '-')}` `reference={vaps.get('reference', '-')}` `match={vaps.get('match', False)}`",

View File

@@ -6,10 +6,15 @@ usage() {
Usage:
./scripts/cir/run_cir_replay_ours.sh \
--cir <path> \
[--static-root <path> | --raw-store-db <path>] \
--repo-bytes-db <path> \
--out-dir <path> \
--reference-ccr <path> \
[--keep-db] \
[--write-actual-ccr] \
[--write-report-json] \
[--report-json-compact] \
[--phase2-object-workers <n>] \
[--phase2-worker-queue-capacity <n>] \
[--rpki-bin <path>] \
[--real-rsync-bin <path>]
EOF
@@ -18,27 +23,34 @@ EOF
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
CIR=""
STATIC_ROOT=""
POOL_DB=""
RAW_STORE_DB=""
REPO_BYTES_DB=""
OUT_DIR=""
REFERENCE_CCR=""
KEEP_DB=0
RPKI_BIN="$ROOT_DIR/target/release/rpki"
CIR_MATERIALIZE_BIN="$ROOT_DIR/target/release/cir_materialize"
CIR_EXTRACT_INPUTS_BIN="$ROOT_DIR/target/release/cir_extract_inputs"
CCR_TO_COMPARE_VIEWS_BIN="$ROOT_DIR/target/release/ccr_to_compare_views"
WRITE_ACTUAL_CCR=0
WRITE_REPORT_JSON=0
REPORT_JSON_COMPACT=0
PHASE2_OBJECT_WORKERS="${CIR_REPLAY_PHASE2_OBJECT_WORKERS:-4}"
PHASE2_WORKER_QUEUE_CAPACITY="${CIR_REPLAY_PHASE2_WORKER_QUEUE_CAPACITY:-64}"
RPKI_BIN="${RPKI_BIN:-$ROOT_DIR/target/release/rpki}"
CIR_MATERIALIZE_BIN="${CIR_MATERIALIZE_BIN:-$ROOT_DIR/target/release/cir_materialize}"
CIR_EXTRACT_INPUTS_BIN="${CIR_EXTRACT_INPUTS_BIN:-$ROOT_DIR/target/release/cir_extract_inputs}"
CCR_TO_COMPARE_VIEWS_BIN="${CCR_TO_COMPARE_VIEWS_BIN:-$ROOT_DIR/target/release/ccr_to_compare_views}"
REAL_RSYNC_BIN="${REAL_RSYNC_BIN:-/usr/bin/rsync}"
WRAPPER="$ROOT_DIR/scripts/cir/cir-rsync-wrapper"
while [[ $# -gt 0 ]]; do
case "$1" in
--cir) CIR="$2"; shift 2 ;;
--static-root) STATIC_ROOT="$2"; shift 2 ;;
--raw-store-db) RAW_STORE_DB="$2"; shift 2 ;;
--repo-bytes-db) REPO_BYTES_DB="$2"; shift 2 ;;
--out-dir) OUT_DIR="$2"; shift 2 ;;
--reference-ccr) REFERENCE_CCR="$2"; shift 2 ;;
--keep-db) KEEP_DB=1; shift ;;
--write-actual-ccr) WRITE_ACTUAL_CCR=1; shift ;;
--write-report-json) WRITE_REPORT_JSON=1; shift ;;
--report-json-compact) WRITE_REPORT_JSON=1; REPORT_JSON_COMPACT=1; shift ;;
--phase2-object-workers) PHASE2_OBJECT_WORKERS="$2"; shift 2 ;;
--phase2-worker-queue-capacity) PHASE2_WORKER_QUEUE_CAPACITY="$2"; shift 2 ;;
--rpki-bin) RPKI_BIN="$2"; shift 2 ;;
--real-rsync-bin) REAL_RSYNC_BIN="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
@@ -46,18 +58,20 @@ while [[ $# -gt 0 ]]; do
esac
done
[[ -n "$CIR" && -n "$OUT_DIR" && -n "$REFERENCE_CCR" ]] || {
[[ -n "$CIR" && -n "$REPO_BYTES_DB" && -n "$OUT_DIR" && -n "$REFERENCE_CCR" ]] || {
usage >&2
exit 2
}
backend_count=0
[[ -n "$STATIC_ROOT" ]] && backend_count=$((backend_count+1))
[[ -n "$RAW_STORE_DB" ]] && backend_count=$((backend_count+1))
[[ "$backend_count" -eq 1 ]] || { usage >&2; exit 2; }
mkdir -p "$OUT_DIR"
needs_build=0
if [[ ! -x "$RPKI_BIN" || ! -x "$CIR_MATERIALIZE_BIN" || ! -x "$CIR_EXTRACT_INPUTS_BIN" || ! -x "$CCR_TO_COMPARE_VIEWS_BIN" ]]; then
needs_build=1
elif [[ "$RPKI_BIN" == "$ROOT_DIR/target/release/rpki" ]] && find "$ROOT_DIR/src" "$ROOT_DIR/Cargo.toml" -newer "$RPKI_BIN" -print -quit | grep -q .; then
needs_build=1
fi
if [[ "$needs_build" -eq 1 ]]; then
(
cd "$ROOT_DIR"
cargo build --release --bin rpki --bin cir_materialize --bin cir_extract_inputs --bin ccr_to_compare_views
@@ -69,6 +83,8 @@ TALS_DIR="$TMP_ROOT/tals"
META_JSON="$TMP_ROOT/meta.json"
MIRROR_ROOT="$TMP_ROOT/mirror"
DB_DIR="$TMP_ROOT/work-db"
REPLAY_RAW_STORE_DB="$TMP_ROOT/replay-raw-store.db"
REPLAY_REPO_BYTES_DB="$TMP_ROOT/replay-repo-bytes.db"
ACTUAL_CCR="$OUT_DIR/actual.ccr"
ACTUAL_REPORT="$OUT_DIR/report.json"
ACTUAL_VRPS="$OUT_DIR/actual-vrps.csv"
@@ -82,12 +98,7 @@ rm -rf "$TMP_ROOT"
mkdir -p "$TMP_ROOT"
"$CIR_EXTRACT_INPUTS_BIN" --cir "$CIR" --tals-dir "$TALS_DIR" --meta-json "$META_JSON"
materialize_cmd=("$CIR_MATERIALIZE_BIN" --cir "$CIR" --mirror-root "$MIRROR_ROOT")
if [[ -n "$STATIC_ROOT" ]]; then
materialize_cmd+=(--static-root "$STATIC_ROOT")
else
materialize_cmd+=(--raw-store-db "$RAW_STORE_DB")
fi
materialize_cmd=("$CIR_MATERIALIZE_BIN" --cir "$CIR" --repo-bytes-db "$REPO_BYTES_DB" --mirror-root "$MIRROR_ROOT")
if [[ "$KEEP_DB" -eq 1 ]]; then
materialize_cmd+=(--keep-db)
fi
@@ -98,11 +109,16 @@ import json,sys
print(json.load(open(sys.argv[1]))["validationTime"])
PY
)"
FIRST_TAL="$(python3 - <<'PY' "$META_JSON"
import json,sys
print(json.load(open(sys.argv[1]))["talFiles"][0]["path"])
mapfile -t TAL_PATHS < <(python3 - <<'PY' "$META_JSON"
import json, sys
for item in json.load(open(sys.argv[1], encoding="utf-8"))["talFiles"]:
print(item["path"])
PY
)"
)
TAL_ARGS=()
for tal_path in "${TAL_PATHS[@]}"; do
TAL_ARGS+=(--tal-path "$tal_path")
done
export CIR_MIRROR_ROOT="$(python3 - <<'PY' "$MIRROR_ROOT"
from pathlib import Path
@@ -113,43 +129,117 @@ PY
export REAL_RSYNC_BIN="$REAL_RSYNC_BIN"
export CIR_LOCAL_LINK_MODE=1
REPORT_JSON_ARGS=(--skip-report-build)
VCIR_ARGS=(--skip-vcir-persist)
if [[ "$WRITE_REPORT_JSON" -eq 1 ]]; then
REPORT_JSON_ARGS=(--report-json "$ACTUAL_REPORT")
if [[ "$REPORT_JSON_COMPACT" -eq 1 ]]; then
REPORT_JSON_ARGS+=(--report-json-compact)
fi
fi
CCR_ARGS=()
if [[ "$WRITE_ACTUAL_CCR" -eq 1 ]]; then
CCR_ARGS=(--ccr-out "$ACTUAL_CCR")
fi
"$RPKI_BIN" \
--db "$DB_DIR" \
--tal-path "$FIRST_TAL" \
--raw-store-db "$REPLAY_RAW_STORE_DB" \
--repo-bytes-db "$REPLAY_REPO_BYTES_DB" \
"${TAL_ARGS[@]}" \
--parallel-phase2-object-workers "$PHASE2_OBJECT_WORKERS" \
--parallel-phase2-worker-queue-capacity "$PHASE2_WORKER_QUEUE_CAPACITY" \
--disable-rrdp \
--rsync-command "$WRAPPER" \
--validation-time "$VALIDATION_TIME" \
--ccr-out "$ACTUAL_CCR" \
--report-json "$ACTUAL_REPORT" \
"${CCR_ARGS[@]}" \
--vrps-csv-out "$ACTUAL_VRPS" \
--vaps-csv-out "$ACTUAL_VAPS" \
--compare-view-trust-anchor unknown \
"${VCIR_ARGS[@]}" \
"${REPORT_JSON_ARGS[@]}" \
>"$RUN_LOG" 2>&1
"$CCR_TO_COMPARE_VIEWS_BIN" --ccr "$ACTUAL_CCR" --vrps-out "$ACTUAL_VRPS" --vaps-out "$ACTUAL_VAPS" --trust-anchor unknown
sort_compare_csv() {
local path="$1"
local tmp="${path}.sorted.tmp"
{
head -n 1 "$path"
tail -n +2 "$path" | LC_ALL=C sort -u
} >"$tmp"
mv "$tmp" "$path"
}
sort_compare_csv "$ACTUAL_VRPS"
sort_compare_csv "$ACTUAL_VAPS"
"$CCR_TO_COMPARE_VIEWS_BIN" --ccr "$REFERENCE_CCR" --vrps-out "$REF_VRPS" --vaps-out "$REF_VAPS" --trust-anchor unknown
python3 - <<'PY' "$ACTUAL_VRPS" "$REF_VRPS" "$ACTUAL_VAPS" "$REF_VAPS" "$COMPARE_JSON"
python3 - <<'PY' "$ACTUAL_VRPS" "$REF_VRPS" "$ACTUAL_VAPS" "$REF_VAPS" "$COMPARE_JSON" "$META_JSON" "$WRITE_REPORT_JSON" "$WRITE_ACTUAL_CCR" "$PHASE2_OBJECT_WORKERS" "$PHASE2_WORKER_QUEUE_CAPACITY"
import csv, json, sys
def rows(path):
with open(path, newline="") as f:
return list(csv.reader(f))[1:]
actual_vrps = {tuple(r) for r in rows(sys.argv[1])}
ref_vrps = {tuple(r) for r in rows(sys.argv[2])}
actual_vaps = {tuple(r) for r in rows(sys.argv[3])}
ref_vaps = {tuple(r) for r in rows(sys.argv[4])}
def next_row(reader):
try:
return tuple(next(reader))
except StopIteration:
return None
def compare_sorted_csv(actual_path, ref_path):
actual_count = 0
ref_count = 0
only_actual_count = 0
only_ref_count = 0
only_actual_sample = []
only_ref_sample = []
with open(actual_path, newline="") as actual_file, open(ref_path, newline="") as ref_file:
actual_reader = csv.reader(actual_file)
ref_reader = csv.reader(ref_file)
next(actual_reader, None)
next(ref_reader, None)
actual = next_row(actual_reader)
ref = next_row(ref_reader)
while actual is not None or ref is not None:
if ref is None or (actual is not None and actual < ref):
actual_count += 1
only_actual_count += 1
if len(only_actual_sample) < 20:
only_actual_sample.append(list(actual))
actual = next_row(actual_reader)
elif actual is None or ref < actual:
ref_count += 1
only_ref_count += 1
if len(only_ref_sample) < 20:
only_ref_sample.append(list(ref))
ref = next_row(ref_reader)
else:
actual_count += 1
ref_count += 1
actual = next_row(actual_reader)
ref = next_row(ref_reader)
return {
"actual": actual_count,
"reference": ref_count,
"only_in_actual": only_actual_sample,
"only_in_reference": only_ref_sample,
"only_in_actual_count": only_actual_count,
"only_in_reference_count": only_ref_count,
"match": only_actual_count == 0 and only_ref_count == 0,
}
vrps = compare_sorted_csv(sys.argv[1], sys.argv[2])
vaps = compare_sorted_csv(sys.argv[3], sys.argv[4])
meta = json.load(open(sys.argv[6], encoding="utf-8"))
summary = {
"vrps": {
"actual": len(actual_vrps),
"reference": len(ref_vrps),
"only_in_actual": sorted(actual_vrps - ref_vrps)[:20],
"only_in_reference": sorted(ref_vrps - actual_vrps)[:20],
"match": actual_vrps == ref_vrps,
"compareMode": "trust-anchor-agnostic",
"talCount": len(meta["talFiles"]),
"talPaths": [item["path"] for item in meta["talFiles"]],
"actualCcrWritten": sys.argv[8] == "1",
"reportJsonWritten": sys.argv[7] == "1",
"replayParallelism": {
"phase2ObjectWorkers": int(sys.argv[9]),
"phase2WorkerQueueCapacity": int(sys.argv[10]),
},
"vaps": {
"actual": len(actual_vaps),
"reference": len(ref_vaps),
"only_in_actual": sorted(actual_vaps - ref_vaps)[:20],
"only_in_reference": sorted(ref_vaps - actual_vaps)[:20],
"match": actual_vaps == ref_vaps,
}
"vrps": vrps,
"vaps": vaps,
}
with open(sys.argv[5], "w") as f:
json.dump(summary, f, indent=2)

View File

@@ -6,7 +6,7 @@ usage() {
Usage:
./scripts/cir/run_cir_replay_routinator.sh \
--cir <path> \
[--static-root <path> | --raw-store-db <path>] \
--repo-bytes-db <path> \
--out-dir <path> \
--reference-ccr <path> \
[--keep-db] \
@@ -20,18 +20,16 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
RPKI_DEV_ROOT="${RPKI_DEV_ROOT:-$ROOT_DIR}"
CIR=""
STATIC_ROOT=""
POOL_DB=""
RAW_STORE_DB=""
REPO_BYTES_DB=""
OUT_DIR=""
REFERENCE_CCR=""
KEEP_DB=0
ROUTINATOR_ROOT="${ROUTINATOR_ROOT:-/home/yuyr/dev/rust_playground/routinator}"
ROUTINATOR_BIN="${ROUTINATOR_BIN:-$ROUTINATOR_ROOT/target/debug/routinator}"
REAL_RSYNC_BIN="${REAL_RSYNC_BIN:-/usr/bin/rsync}"
CIR_MATERIALIZE_BIN="$ROOT_DIR/target/release/cir_materialize"
CIR_EXTRACT_INPUTS_BIN="$ROOT_DIR/target/release/cir_extract_inputs"
CCR_TO_COMPARE_VIEWS_BIN="$ROOT_DIR/target/release/ccr_to_compare_views"
CIR_MATERIALIZE_BIN="${CIR_MATERIALIZE_BIN:-$ROOT_DIR/target/release/cir_materialize}"
CIR_EXTRACT_INPUTS_BIN="${CIR_EXTRACT_INPUTS_BIN:-$ROOT_DIR/target/release/cir_extract_inputs}"
CCR_TO_COMPARE_VIEWS_BIN="${CCR_TO_COMPARE_VIEWS_BIN:-$ROOT_DIR/target/release/ccr_to_compare_views}"
WRAPPER="$ROOT_DIR/scripts/cir/cir-rsync-wrapper"
JSON_TO_VAPS="$ROOT_DIR/scripts/cir/json_to_vaps_csv.py"
FAKETIME_LIB="${FAKETIME_LIB:-$ROOT_DIR/target/tools/faketime_pkg/extracted/libfaketime/usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1}"
@@ -39,8 +37,7 @@ FAKETIME_LIB="${FAKETIME_LIB:-$ROOT_DIR/target/tools/faketime_pkg/extracted/libf
while [[ $# -gt 0 ]]; do
case "$1" in
--cir) CIR="$2"; shift 2 ;;
--static-root) STATIC_ROOT="$2"; shift 2 ;;
--raw-store-db) RAW_STORE_DB="$2"; shift 2 ;;
--repo-bytes-db) REPO_BYTES_DB="$2"; shift 2 ;;
--out-dir) OUT_DIR="$2"; shift 2 ;;
--reference-ccr) REFERENCE_CCR="$2"; shift 2 ;;
--keep-db) KEEP_DB=1; shift ;;
@@ -52,14 +49,14 @@ while [[ $# -gt 0 ]]; do
esac
done
[[ -n "$CIR" && -n "$OUT_DIR" && -n "$REFERENCE_CCR" ]] || {
[[ -n "$CIR" && -n "$REPO_BYTES_DB" && -n "$OUT_DIR" && -n "$REFERENCE_CCR" ]] || {
usage >&2
exit 2
}
backend_count=0
[[ -n "$STATIC_ROOT" ]] && backend_count=$((backend_count+1))
[[ -n "$RAW_STORE_DB" ]] && backend_count=$((backend_count+1))
[[ "$backend_count" -eq 1 ]] || { usage >&2; exit 2; }
if [[ ! -x "$ROUTINATOR_BIN" ]]; then
echo "routinator binary not executable: $ROUTINATOR_BIN" >&2
exit 2
fi
mkdir -p "$OUT_DIR"
if [[ ! -x "$CIR_MATERIALIZE_BIN" || ! -x "$CIR_EXTRACT_INPUTS_BIN" || ! -x "$CCR_TO_COMPARE_VIEWS_BIN" ]]; then
@@ -102,12 +99,7 @@ for tal in Path(sys.argv[1]).glob("*.tal"):
seen_sep = True
tal.write_text("\n".join(rsync_uris) + "\n\n" + "\n".join(base64_lines) + "\n", encoding="utf-8")
PY
materialize_cmd=("$CIR_MATERIALIZE_BIN" --cir "$CIR" --mirror-root "$MIRROR_ROOT")
if [[ -n "$STATIC_ROOT" ]]; then
materialize_cmd+=(--static-root "$STATIC_ROOT")
else
materialize_cmd+=(--raw-store-db "$RAW_STORE_DB")
fi
materialize_cmd=("$CIR_MATERIALIZE_BIN" --cir "$CIR" --repo-bytes-db "$REPO_BYTES_DB" --mirror-root "$MIRROR_ROOT")
if [[ "$KEEP_DB" -eq 1 ]]; then
materialize_cmd+=(--keep-db)
fi
@@ -118,12 +110,13 @@ import json,sys
print(json.load(open(sys.argv[1]))["validationTime"])
PY
)"
FIRST_TAL="$(python3 - <<'PY' "$META_JSON"
import json,sys
print(json.load(open(sys.argv[1]))["talFiles"][0]["path"])
mapfile -t TAL_PATHS < <(python3 - <<'PY' "$META_JSON"
import json, sys
for item in json.load(open(sys.argv[1], encoding="utf-8"))["talFiles"]:
print(item["path"])
PY
)"
TA_NAME="$(basename "$FIRST_TAL" .tal)"
)
COMPARE_TRUST_ANCHOR="unknown"
FAKE_EPOCH="$(python3 - <<'PY' "$VALIDATION_TIME"
from datetime import datetime, timezone
import sys
@@ -183,9 +176,30 @@ env \
vrps --noupdate --format json -o "$ACTUAL_VAPS_JSON" >>"$RUN_LOG" 2>&1
python3 "$JSON_TO_VAPS" --input "$ACTUAL_VAPS_JSON" --csv-out "$ACTUAL_VAPS"
"$CCR_TO_COMPARE_VIEWS_BIN" --ccr "$REFERENCE_CCR" --vrps-out "$REF_VRPS" --vaps-out "$REF_VAPS" --trust-anchor "$TA_NAME"
python3 - <<'PY' "$ACTUAL_VRPS" "$REF_VRPS" "$ACTUAL_VAPS" "$REF_VAPS" "$SUMMARY_JSON"
normalize_trust_anchor_csv() {
python3 - <<'PY' "$1" "$2"
import csv
import sys
from pathlib import Path
path = Path(sys.argv[1])
trust_anchor = sys.argv[2]
rows = list(csv.reader(path.open(newline="", encoding="utf-8")))
if rows:
for row in rows[1:]:
if row:
row[-1] = trust_anchor
with path.open("w", newline="", encoding="utf-8") as fh:
csv.writer(fh).writerows(rows)
PY
}
normalize_trust_anchor_csv "$ACTUAL_VRPS" "$COMPARE_TRUST_ANCHOR"
normalize_trust_anchor_csv "$ACTUAL_VAPS" "$COMPARE_TRUST_ANCHOR"
"$CCR_TO_COMPARE_VIEWS_BIN" --ccr "$REFERENCE_CCR" --vrps-out "$REF_VRPS" --vaps-out "$REF_VAPS" --trust-anchor "$COMPARE_TRUST_ANCHOR"
python3 - <<'PY' "$ACTUAL_VRPS" "$REF_VRPS" "$ACTUAL_VAPS" "$REF_VAPS" "$SUMMARY_JSON" "$META_JSON"
import csv, json, sys
def rows(path):
with open(path, newline="") as f:
@@ -194,7 +208,11 @@ actual_vrps = {tuple(r) for r in rows(sys.argv[1])}
ref_vrps = {tuple(r) for r in rows(sys.argv[2])}
actual_vaps = {tuple(r) for r in rows(sys.argv[3])}
ref_vaps = {tuple(r) for r in rows(sys.argv[4])}
meta = json.load(open(sys.argv[6], encoding="utf-8"))
summary = {
"compareMode": "trust-anchor-agnostic",
"talCount": len(meta["talFiles"]),
"talPaths": [item["path"] for item in meta["talFiles"]],
"vrps": {
"actual": len(actual_vrps),
"reference": len(ref_vrps),

View File

@@ -6,10 +6,10 @@ usage() {
Usage:
./scripts/cir/run_cir_replay_rpki_client.sh \
--cir <path> \
[--static-root <path> | --raw-store-db <path>] \
--repo-bytes-db <path> \
--out-dir <path> \
--reference-ccr <path> \
--build-dir <path> \
[--build-dir <path> | --rpki-client-bin <path>] \
[--keep-db] \
[--real-rsync-bin <path>]
EOF
@@ -17,27 +17,26 @@ EOF
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
CIR=""
STATIC_ROOT=""
POOL_DB=""
RAW_STORE_DB=""
REPO_BYTES_DB=""
OUT_DIR=""
REFERENCE_CCR=""
BUILD_DIR=""
RPKI_CLIENT_BIN=""
KEEP_DB=0
REAL_RSYNC_BIN="${REAL_RSYNC_BIN:-/usr/bin/rsync}"
CIR_MATERIALIZE_BIN="$ROOT_DIR/target/release/cir_materialize"
CIR_EXTRACT_INPUTS_BIN="$ROOT_DIR/target/release/cir_extract_inputs"
CCR_TO_COMPARE_VIEWS_BIN="$ROOT_DIR/target/release/ccr_to_compare_views"
CIR_MATERIALIZE_BIN="${CIR_MATERIALIZE_BIN:-$ROOT_DIR/target/release/cir_materialize}"
CIR_EXTRACT_INPUTS_BIN="${CIR_EXTRACT_INPUTS_BIN:-$ROOT_DIR/target/release/cir_extract_inputs}"
CCR_TO_COMPARE_VIEWS_BIN="${CCR_TO_COMPARE_VIEWS_BIN:-$ROOT_DIR/target/release/ccr_to_compare_views}"
WRAPPER="$ROOT_DIR/scripts/cir/cir-rsync-wrapper"
while [[ $# -gt 0 ]]; do
case "$1" in
--cir) CIR="$2"; shift 2 ;;
--static-root) STATIC_ROOT="$2"; shift 2 ;;
--raw-store-db) RAW_STORE_DB="$2"; shift 2 ;;
--repo-bytes-db) REPO_BYTES_DB="$2"; shift 2 ;;
--out-dir) OUT_DIR="$2"; shift 2 ;;
--reference-ccr) REFERENCE_CCR="$2"; shift 2 ;;
--build-dir) BUILD_DIR="$2"; shift 2 ;;
--rpki-client-bin) RPKI_CLIENT_BIN="$2"; shift 2 ;;
--keep-db) KEEP_DB=1; shift ;;
--real-rsync-bin) REAL_RSYNC_BIN="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
@@ -45,14 +44,21 @@ while [[ $# -gt 0 ]]; do
esac
done
[[ -n "$CIR" && -n "$OUT_DIR" && -n "$REFERENCE_CCR" && -n "$BUILD_DIR" ]] || {
[[ -n "$CIR" && -n "$REPO_BYTES_DB" && -n "$OUT_DIR" && -n "$REFERENCE_CCR" ]] || {
usage >&2
exit 2
}
backend_count=0
[[ -n "$STATIC_ROOT" ]] && backend_count=$((backend_count+1))
[[ -n "$RAW_STORE_DB" ]] && backend_count=$((backend_count+1))
[[ "$backend_count" -eq 1 ]] || { usage >&2; exit 2; }
if [[ -z "$BUILD_DIR" && -z "$RPKI_CLIENT_BIN" ]]; then
usage >&2
exit 2
fi
if [[ -z "$RPKI_CLIENT_BIN" ]]; then
RPKI_CLIENT_BIN="$BUILD_DIR/src/rpki-client"
fi
if [[ ! -x "$RPKI_CLIENT_BIN" ]]; then
echo "rpki-client binary not executable: $RPKI_CLIENT_BIN" >&2
exit 2
fi
mkdir -p "$OUT_DIR"
if [[ ! -x "$CIR_MATERIALIZE_BIN" || ! -x "$CIR_EXTRACT_INPUTS_BIN" || ! -x "$CCR_TO_COMPARE_VIEWS_BIN" ]]; then
@@ -97,12 +103,7 @@ for tal in Path(sys.argv[1]).glob("*.tal"):
seen_sep = True
tal.write_text("\n".join(rsync_uris) + "\n\n" + "\n".join(base64_lines) + "\n", encoding="utf-8")
PY
materialize_cmd=("$CIR_MATERIALIZE_BIN" --cir "$CIR" --mirror-root "$MIRROR_ROOT")
if [[ -n "$STATIC_ROOT" ]]; then
materialize_cmd+=(--static-root "$STATIC_ROOT")
else
materialize_cmd+=(--raw-store-db "$RAW_STORE_DB")
fi
materialize_cmd=("$CIR_MATERIALIZE_BIN" --cir "$CIR" --repo-bytes-db "$REPO_BYTES_DB" --mirror-root "$MIRROR_ROOT")
if [[ "$KEEP_DB" -eq 1 ]]; then
materialize_cmd+=(--keep-db)
fi
@@ -116,12 +117,17 @@ dt = datetime.fromisoformat(vt.replace("Z", "+00:00")).astimezone(timezone.utc)
print(int(dt.timestamp()))
PY
)"
FIRST_TAL="$(python3 - <<'PY' "$META_JSON"
import json,sys
print(json.load(open(sys.argv[1]))["talFiles"][0]["path"])
mapfile -t TAL_PATHS < <(python3 - <<'PY' "$META_JSON"
import json, sys
for item in json.load(open(sys.argv[1], encoding="utf-8"))["talFiles"]:
print(item["path"])
PY
)"
TA_NAME="$(basename "$FIRST_TAL" .tal)"
)
CLIENT_TAL_ARGS=()
for tal_path in "${TAL_PATHS[@]}"; do
CLIENT_TAL_ARGS+=(-t "$tal_path")
done
COMPARE_TRUST_ANCHOR="unknown"
export CIR_MIRROR_ROOT="$(python3 - <<'PY' "$MIRROR_ROOT"
from pathlib import Path
@@ -133,19 +139,62 @@ export REAL_RSYNC_BIN="$REAL_RSYNC_BIN"
export CIR_LOCAL_LINK_MODE=1
mkdir -p "$CACHE_DIR" "$OUT_CCR_DIR"
"$BUILD_DIR/src/rpki-client" \
chmod -R 0777 "$TMP_ROOT"
"$RPKI_CLIENT_BIN" \
-R \
-e "$WRAPPER" \
-P "$VALIDATION_EPOCH" \
-t "$FIRST_TAL" \
"${CLIENT_TAL_ARGS[@]}" \
-d "$CACHE_DIR" \
"$OUT_CCR_DIR" >"$RUN_LOG" 2>&1
"$CCR_TO_COMPARE_VIEWS_BIN" \
--ccr "$OUT_CCR_DIR/rpki.ccr" \
--vrps-out "$ACTUAL_VRPS" \
--vaps-out "$ACTUAL_VAPS" \
--trust-anchor "$TA_NAME"
if [[ -f "$OUT_CCR_DIR/rpki.ccr" ]]; then
"$CCR_TO_COMPARE_VIEWS_BIN" \
--ccr "$OUT_CCR_DIR/rpki.ccr" \
--vrps-out "$ACTUAL_VRPS" \
--vaps-out "$ACTUAL_VAPS" \
--trust-anchor "$COMPARE_TRUST_ANCHOR"
else
python3 - <<'PY' "$OUT_CCR_DIR/json" "$ACTUAL_VRPS" "$ACTUAL_VAPS" "$COMPARE_TRUST_ANCHOR"
import csv
import json
import sys
from pathlib import Path
json_path = Path(sys.argv[1])
vrps_out = Path(sys.argv[2])
vaps_out = Path(sys.argv[3])
compare_ta = sys.argv[4]
if not json_path.is_file():
raise SystemExit(f"rpki-client output has neither rpki.ccr nor json: {json_path}")
data = json.loads(json_path.read_text(encoding="utf-8"))
vrps_out.parent.mkdir(parents=True, exist_ok=True)
with vrps_out.open("w", newline="", encoding="utf-8") as fh:
writer = csv.writer(fh)
writer.writerow(["ASN", "IP Prefix", "Max Length", "Trust Anchor"])
for roa in data.get("roas", []):
writer.writerow([
f"AS{roa['asn']}",
roa["prefix"],
str(roa["maxLength"]),
compare_ta,
])
with vaps_out.open("w", newline="", encoding="utf-8") as fh:
writer = csv.writer(fh)
writer.writerow(["Customer ASN", "Providers", "Trust Anchor"])
for aspa in data.get("aspas", []):
providers = ";".join(f"AS{item}" for item in sorted(aspa.get("providers", [])))
writer.writerow([
f"AS{aspa['customer_asid']}",
providers,
compare_ta,
])
PY
fi
python3 - <<'PY' "$ACTUAL_VRPS" "$ACTUAL_VAPS" "$ACTUAL_VRPS_META" "$ACTUAL_VAPS_META"
import csv, json, sys
@@ -157,9 +206,9 @@ json.dump({"count": count_rows(sys.argv[1])}, open(sys.argv[3], "w"), indent=2)
json.dump({"count": count_rows(sys.argv[2])}, open(sys.argv[4], "w"), indent=2)
PY
"$CCR_TO_COMPARE_VIEWS_BIN" --ccr "$REFERENCE_CCR" --vrps-out "$REF_VRPS" --vaps-out "$REF_VAPS" --trust-anchor "$TA_NAME"
"$CCR_TO_COMPARE_VIEWS_BIN" --ccr "$REFERENCE_CCR" --vrps-out "$REF_VRPS" --vaps-out "$REF_VAPS" --trust-anchor "$COMPARE_TRUST_ANCHOR"
python3 - <<'PY' "$ACTUAL_VRPS" "$REF_VRPS" "$ACTUAL_VAPS" "$REF_VAPS" "$SUMMARY_JSON"
python3 - <<'PY' "$ACTUAL_VRPS" "$REF_VRPS" "$ACTUAL_VAPS" "$REF_VAPS" "$SUMMARY_JSON" "$META_JSON"
import csv, json, sys
def rows(path):
with open(path, newline="") as f:
@@ -168,7 +217,11 @@ actual_vrps = {tuple(r) for r in rows(sys.argv[1])}
ref_vrps = {tuple(r) for r in rows(sys.argv[2])}
actual_vaps = {tuple(r) for r in rows(sys.argv[3])}
ref_vaps = {tuple(r) for r in rows(sys.argv[4])}
meta = json.load(open(sys.argv[6], encoding="utf-8"))
summary = {
"compareMode": "trust-anchor-agnostic",
"talCount": len(meta["talFiles"]),
"talPaths": [item["path"] for item in meta["talFiles"]],
"vrps": {
"actual": len(actual_vrps),
"reference": len(ref_vrps),

View File

@@ -55,11 +55,7 @@ rpki_bin = sys.argv[6]
real_rsync_bin = sys.argv[7]
sequence = json.loads((sequence_root / "sequence.json").read_text(encoding="utf-8"))
static_root = sequence_root / sequence["staticRoot"] if "staticRoot" in sequence else None
raw_store_db = sequence_root / sequence["rawStoreDbPath"] if "rawStoreDbPath" in sequence else None
backend_count = sum(x is not None for x in (static_root, raw_store_db))
if backend_count != 1:
raise SystemExit("sequence must set exactly one of staticRoot or rawStoreDbPath")
repo_bytes_db = sequence_root / sequence["repoBytesDbPath"]
steps = sequence["steps"]
results = []
@@ -81,10 +77,7 @@ for step in steps:
"--real-rsync-bin",
real_rsync_bin,
]
if static_root is not None:
cmd.extend(["--static-root", str(static_root)])
else:
cmd.extend(["--raw-store-db", str(raw_store_db)])
cmd.extend(["--repo-bytes-db", str(repo_bytes_db)])
proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode != 0:
raise SystemExit(
@@ -99,6 +92,9 @@ for step in steps:
"outDir": str(out_dir),
"comparePath": str(out_dir / "compare-summary.json"),
"timingPath": str(out_dir / "timing.json"),
"compareMode": compare.get("compareMode"),
"talCount": compare.get("talCount"),
"talPaths": compare.get("talPaths", []),
"compare": compare,
"timing": timing,
"match": bool(compare["vrps"]["match"]) and bool(compare["vaps"]["match"]),
@@ -124,16 +120,18 @@ lines = [
f"- `step_count`: `{len(results)}`",
f"- `all_match`: `{all_match}`",
"",
"| Step | Kind | VRP actual/ref | VRP match | VAP actual/ref | VAP match | Duration (ms) |",
"| --- | --- | --- | --- | --- | --- | ---: |",
"| Step | Kind | TALs | Compare mode | VRP actual/ref | VRP match | VAP actual/ref | VAP match | Duration (ms) |",
"| --- | --- | ---: | --- | --- | --- | --- | --- | ---: |",
]
for item in results:
compare = item["compare"]
timing = item.get("timing") or {}
lines.append(
"| {step} | {kind} | {va}/{vr} | {vm} | {aa}/{ar} | {am} | {dur} |".format(
"| {step} | {kind} | {tal_count} | {compare_mode} | {va}/{vr} | {vm} | {aa}/{ar} | {am} | {dur} |".format(
step=item["stepId"],
kind=item["kind"],
tal_count=item.get("talCount") if item.get("talCount") is not None else "-",
compare_mode=item.get("compareMode") or "-",
va=compare["vrps"]["actual"],
vr=compare["vrps"]["reference"],
vm=compare["vrps"]["match"],

View File

@@ -57,11 +57,7 @@ routinator_bin = sys.argv[6]
real_rsync_bin = sys.argv[7]
sequence = json.loads((sequence_root / "sequence.json").read_text(encoding="utf-8"))
static_root = sequence_root / sequence["staticRoot"] if "staticRoot" in sequence else None
raw_store_db = sequence_root / sequence["rawStoreDbPath"] if "rawStoreDbPath" in sequence else None
backend_count = sum(x is not None for x in (static_root, raw_store_db))
if backend_count != 1:
raise SystemExit("sequence must set exactly one of staticRoot or rawStoreDbPath")
repo_bytes_db = sequence_root / sequence["repoBytesDbPath"]
steps = sequence["steps"]
results = []
all_match = True
@@ -85,10 +81,7 @@ for step in steps:
"--real-rsync-bin",
real_rsync_bin,
]
if static_root is not None:
cmd.extend(["--static-root", str(static_root)])
else:
cmd.extend(["--raw-store-db", str(raw_store_db)])
cmd.extend(["--repo-bytes-db", str(repo_bytes_db)])
proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode != 0:
raise SystemExit(
@@ -104,6 +97,9 @@ for step in steps:
"validationTime": step["validationTime"],
"outDir": str(out_dir),
"comparePath": str(out_dir / "compare-summary.json"),
"compareMode": compare.get("compareMode"),
"talCount": compare.get("talCount"),
"talPaths": compare.get("talPaths", []),
"match": match,
"compare": compare,
}
@@ -125,7 +121,25 @@ lines = [
f"- `step_count`: `{len(results)}`",
f"- `all_match`: `{all_match}`",
"",
"| Step | Kind | TALs | Compare mode | VRP actual/ref | VRP match | VAP actual/ref | VAP match |",
"| --- | --- | ---: | --- | --- | --- | --- | --- |",
]
for item in results:
compare = item["compare"]
lines.append(
"| {step} | {kind} | {tal_count} | {compare_mode} | {va}/{vr} | {vm} | {aa}/{ar} | {am} |".format(
step=item["stepId"],
kind=item["kind"],
tal_count=item.get("talCount") if item.get("talCount") is not None else "-",
compare_mode=item.get("compareMode") or "-",
va=compare["vrps"]["actual"],
vr=compare["vrps"]["reference"],
vm=compare["vrps"]["match"],
aa=compare["vaps"]["actual"],
ar=compare["vaps"]["reference"],
am=compare["vaps"]["match"],
)
)
summary_md.write_text("\n".join(lines), encoding="utf-8")
PY

View File

@@ -6,7 +6,7 @@ usage() {
Usage:
./scripts/cir/run_cir_replay_sequence_rpki_client.sh \
--sequence-root <path> \
--build-dir <path> \
[--build-dir <path> | --rpki-client-bin <path>] \
[--real-rsync-bin <path>]
EOF
}
@@ -14,6 +14,7 @@ EOF
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
SEQUENCE_ROOT=""
BUILD_DIR=""
RPKI_CLIENT_BIN=""
REAL_RSYNC_BIN="${REAL_RSYNC_BIN:-/usr/bin/rsync}"
STEP_SCRIPT="$ROOT_DIR/scripts/cir/run_cir_replay_rpki_client.sh"
@@ -21,13 +22,14 @@ while [[ $# -gt 0 ]]; do
case "$1" in
--sequence-root) SEQUENCE_ROOT="$2"; shift 2 ;;
--build-dir) BUILD_DIR="$2"; shift 2 ;;
--rpki-client-bin) RPKI_CLIENT_BIN="$2"; shift 2 ;;
--real-rsync-bin) REAL_RSYNC_BIN="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
esac
done
[[ -n "$SEQUENCE_ROOT" && -n "$BUILD_DIR" ]] || { usage >&2; exit 2; }
[[ -n "$SEQUENCE_ROOT" && ( -n "$BUILD_DIR" || -n "$RPKI_CLIENT_BIN" ) ]] || { usage >&2; exit 2; }
SEQUENCE_ROOT="$(python3 - <<'PY' "$SEQUENCE_ROOT"
from pathlib import Path
@@ -39,7 +41,7 @@ PY
SUMMARY_JSON="$SEQUENCE_ROOT/sequence-summary-rpki-client.json"
SUMMARY_MD="$SEQUENCE_ROOT/sequence-summary-rpki-client.md"
python3 - <<'PY' "$SEQUENCE_ROOT" "$SUMMARY_JSON" "$SUMMARY_MD" "$STEP_SCRIPT" "$BUILD_DIR" "$REAL_RSYNC_BIN"
python3 - <<'PY' "$SEQUENCE_ROOT" "$SUMMARY_JSON" "$SUMMARY_MD" "$STEP_SCRIPT" "$BUILD_DIR" "$RPKI_CLIENT_BIN" "$REAL_RSYNC_BIN"
import json
import subprocess
import sys
@@ -50,14 +52,11 @@ summary_json = Path(sys.argv[2])
summary_md = Path(sys.argv[3])
step_script = Path(sys.argv[4])
build_dir = sys.argv[5]
real_rsync_bin = sys.argv[6]
rpki_client_bin = sys.argv[6]
real_rsync_bin = sys.argv[7]
sequence = json.loads((sequence_root / "sequence.json").read_text(encoding="utf-8"))
static_root = sequence_root / sequence["staticRoot"] if "staticRoot" in sequence else None
raw_store_db = sequence_root / sequence["rawStoreDbPath"] if "rawStoreDbPath" in sequence else None
backend_count = sum(x is not None for x in (static_root, raw_store_db))
if backend_count != 1:
raise SystemExit("sequence must set exactly one of staticRoot or rawStoreDbPath")
repo_bytes_db = sequence_root / sequence["repoBytesDbPath"]
steps = sequence["steps"]
results = []
all_match = True
@@ -74,15 +73,14 @@ for step in steps:
str(out_dir),
"--reference-ccr",
str(sequence_root / step["ccrPath"]),
"--build-dir",
build_dir,
"--real-rsync-bin",
real_rsync_bin,
]
if static_root is not None:
cmd.extend(["--static-root", str(static_root)])
if rpki_client_bin:
cmd.extend(["--rpki-client-bin", rpki_client_bin])
else:
cmd.extend(["--raw-store-db", str(raw_store_db)])
cmd.extend(["--build-dir", build_dir])
cmd.extend(["--repo-bytes-db", str(repo_bytes_db)])
proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode != 0:
raise SystemExit(
@@ -98,6 +96,9 @@ for step in steps:
"validationTime": step["validationTime"],
"outDir": str(out_dir),
"comparePath": str(out_dir / "compare-summary.json"),
"compareMode": compare.get("compareMode"),
"talCount": compare.get("talCount"),
"talPaths": compare.get("talPaths", []),
"match": match,
"compare": compare,
}
@@ -119,7 +120,25 @@ lines = [
f"- `step_count`: `{len(results)}`",
f"- `all_match`: `{all_match}`",
"",
"| Step | Kind | TALs | Compare mode | VRP actual/ref | VRP match | VAP actual/ref | VAP match |",
"| --- | --- | ---: | --- | --- | --- | --- | --- |",
]
for item in results:
compare = item["compare"]
lines.append(
"| {step} | {kind} | {tal_count} | {compare_mode} | {va}/{vr} | {vm} | {aa}/{ar} | {am} |".format(
step=item["stepId"],
kind=item["kind"],
tal_count=item.get("talCount") if item.get("talCount") is not None else "-",
compare_mode=item.get("compareMode") or "-",
va=compare["vrps"]["actual"],
vr=compare["vrps"]["reference"],
vm=compare["vrps"]["match"],
aa=compare["vaps"]["actual"],
ar=compare["vaps"]["reference"],
am=compare["vaps"]["match"],
)
)
summary_md.write_text("\n".join(lines), encoding="utf-8")
PY

View File

@@ -0,0 +1,502 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
./scripts/compare/run_perf_compare_quick_remote.sh \
--run-root <path> \
--remote-root <path> \
[--rir-set <mixed2|all5>] \
[--ssh-target <user@host>] \
[--rpki-client-bin <path>] \
[--libtls-path <path>] \
[--rp-run-mode <serial|parallel>] \
[--copy-rpki-client-cache] \
[--probe-rpki-client-cache] \
[--ours-extra-args '<args>'] \
[--dry-run]
EOF
}
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
first_existing_executable() {
local fallback="$1"
shift
local candidate
for candidate in "$@"; do
if [[ -x "$candidate" ]]; then
printf '%s' "$candidate"
return
fi
done
printf '%s' "$fallback"
}
first_existing_file() {
local fallback="$1"
shift
local candidate
for candidate in "$@"; do
if [[ -f "$candidate" ]]; then
printf '%s' "$candidate"
return
fi
done
printf '%s' "$fallback"
}
RUN_ROOT=""
REMOTE_ROOT=""
SSH_TARGET="${SSH_TARGET:-root@47.251.56.108}"
RPKI_CLIENT_BIN="${RPKI_CLIENT_BIN:-$(first_existing_executable \
"/home/yuyr/dev/rpki-client-9.7/build-m5/src/rpki-client" \
"$ROOT_DIR/../../.cache/rpki-client-9.7-build/bin/rpki-client" \
"$ROOT_DIR/../../.cache/rpki-client-9.7-build/src/rpki-client-9.7/src/rpki-client" \
"$ROOT_DIR/../../.cache/rpki-client-remote9.0/rpki-client" \
"/home/yuyr/dev/rpki-client-9.7/build-m5/src/rpki-client")}"
LIBTLS_PATH="${LIBTLS_PATH:-$(first_existing_file \
"/home/yuyr/dev/rpki-client-9.7/.deps/libtls/root/usr/lib/x86_64-linux-gnu/libtls.so.28.0.0" \
"$ROOT_DIR/../../.cache/rpki-client-9.7-build/runlib/libtls.so.28" \
"$ROOT_DIR/../../.cache/rpki-client-9.7-build/sysroot/usr/lib/x86_64-linux-gnu/libtls.so.28.0.0" \
"$ROOT_DIR/../../.cache/rpki-client-remote9.0/libtls.so.28" \
"/home/yuyr/dev/rpki-client-9.7/.deps/libtls/root/usr/lib/x86_64-linux-gnu/libtls.so.28.0.0")}"
RP_RUN_MODE="${RP_RUN_MODE:-serial}"
RIR_SET="${RIR_SET:-mixed2}"
OURS_EXTRA_ARGS="${OURS_EXTRA_ARGS:-}"
COPY_RPKI_CLIENT_CACHE="${COPY_RPKI_CLIENT_CACHE:-0}"
PROBE_RPKI_CLIENT_CACHE="${PROBE_RPKI_CLIENT_CACHE:-0}"
DRY_RUN=0
while [[ $# -gt 0 ]]; do
case "$1" in
--run-root) RUN_ROOT="$2"; shift 2 ;;
--remote-root) REMOTE_ROOT="$2"; shift 2 ;;
--rir-set) RIR_SET="$2"; shift 2 ;;
--ssh-target) SSH_TARGET="$2"; shift 2 ;;
--rpki-client-bin) RPKI_CLIENT_BIN="$2"; shift 2 ;;
--libtls-path) LIBTLS_PATH="$2"; shift 2 ;;
--rp-run-mode) RP_RUN_MODE="$2"; shift 2 ;;
--copy-rpki-client-cache) COPY_RPKI_CLIENT_CACHE=1; shift ;;
--probe-rpki-client-cache) PROBE_RPKI_CLIENT_CACHE=1; shift ;;
--ours-extra-args) OURS_EXTRA_ARGS="$2"; shift 2 ;;
--dry-run) DRY_RUN=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
esac
done
[[ -n "$RUN_ROOT" && -n "$REMOTE_ROOT" ]] || { usage >&2; exit 2; }
[[ "$RP_RUN_MODE" == "serial" || "$RP_RUN_MODE" == "parallel" ]] || { echo "invalid --rp-run-mode: $RP_RUN_MODE" >&2; usage; exit 2; }
[[ "$RIR_SET" == "mixed2" || "$RIR_SET" == "all5" ]] || { echo "invalid --rir-set: $RIR_SET" >&2; usage; exit 2; }
[[ "$DRY_RUN" -eq 1 || -x "$RPKI_CLIENT_BIN" ]] || { echo "rpki-client binary not executable: $RPKI_CLIENT_BIN" >&2; exit 2; }
[[ "$DRY_RUN" -eq 1 || -f "$LIBTLS_PATH" ]] || { echo "libtls not found: $LIBTLS_PATH" >&2; exit 2; }
RUN_ROOT="$(python3 - <<'PY' "$RUN_ROOT"
from pathlib import Path
import sys
print(Path(sys.argv[1]).resolve())
PY
)"
mkdir -p "$RUN_ROOT/steps/step-001/ours" "$RUN_ROOT/steps/step-001/rpki-client" "$RUN_ROOT/steps/step-001/compare"
mkdir -p "$RUN_ROOT/steps/step-002/ours" "$RUN_ROOT/steps/step-002/rpki-client" "$RUN_ROOT/steps/step-002/compare"
tal_path_for_rir() {
case "$1" in
afrinic) printf '%s' "$ROOT_DIR/tests/fixtures/tal/afrinic.tal" ;;
apnic) printf '%s' "$ROOT_DIR/tests/fixtures/tal/apnic-rfc7730-https.tal" ;;
arin) printf '%s' "$ROOT_DIR/tests/fixtures/tal/arin.tal" ;;
lacnic) printf '%s' "$ROOT_DIR/tests/fixtures/tal/lacnic.tal" ;;
ripe) printf '%s' "$ROOT_DIR/tests/fixtures/tal/ripe-ncc.tal" ;;
*) echo "unknown rir: $1" >&2; exit 2 ;;
esac
}
ta_path_for_rir() {
case "$1" in
afrinic) printf '%s' "$ROOT_DIR/tests/fixtures/ta/afrinic-ta.cer" ;;
apnic) printf '%s' "$ROOT_DIR/tests/fixtures/ta/apnic-ta.cer" ;;
arin) printf '%s' "$ROOT_DIR/tests/fixtures/ta/arin-ta.cer" ;;
lacnic) printf '%s' "$ROOT_DIR/tests/fixtures/ta/lacnic-ta.cer" ;;
ripe) printf '%s' "$ROOT_DIR/tests/fixtures/ta/ripe-ncc-ta.cer" ;;
*) echo "unknown rir: $1" >&2; exit 2 ;;
esac
}
case "$RIR_SET" in
mixed2)
RIRS=(apnic arin)
SCOPE_LABEL="APNIC+ARIN mixed release two-step synchronized compare"
;;
all5)
RIRS=(afrinic apnic arin lacnic ripe)
SCOPE_LABEL="all-five-RIR mixed release two-step synchronized compare"
;;
esac
COPY_FILES=()
for rir in "${RIRS[@]}"; do
COPY_FILES+=("$(tal_path_for_rir "$rir")" "$(ta_path_for_rir "$rir")")
done
if [[ "$DRY_RUN" -eq 1 ]]; then
cat <<EOF2
workflow_name=性能对比测试快速版
scope=$SCOPE_LABEL
rir_set=$RIR_SET
rirs=${RIRS[*]}
run_root=$RUN_ROOT
remote_root=$REMOTE_ROOT
ssh_target=$SSH_TARGET
rp_run_mode=$RP_RUN_MODE
ours_extra_args=$OURS_EXTRA_ARGS
EOF2
exit 0
fi
cleanup_remote() {
if [[ "${KEEP_REMOTE:-0}" != "1" ]]; then
ssh "$SSH_TARGET" "rm -rf '$REMOTE_ROOT'" >/dev/null 2>&1 || true
fi
}
trap cleanup_remote EXIT
(
cd "$ROOT_DIR"
cargo build --release --bin rpki --bin ccr_to_compare_views --bin ccr_state_compare --bin cir_state_compare --bin cir_probe_rpki_client_cache
)
ssh "$SSH_TARGET" "set -e; systemctl disable --now rpki-client.timer >/dev/null 2>&1 || true; systemctl stop rpki-client.service >/dev/null 2>&1 || true; pkill -f '[/]rpki-client([[:space:]]|$)' >/dev/null 2>&1 || true; pkill -f '[/]routinator([[:space:]]|$)' >/dev/null 2>&1 || true; id -u _rpki-client >/dev/null 2>&1 || useradd -r -M -s /usr/sbin/nologin _rpki-client || true; rm -rf '$REMOTE_ROOT'; mkdir -p '$REMOTE_ROOT/bin' '$REMOTE_ROOT/lib' '$REMOTE_ROOT/state/ours' '$REMOTE_ROOT/state/rpki-client' '$REMOTE_ROOT/steps/step-001/ours' '$REMOTE_ROOT/steps/step-001/rpki-client' '$REMOTE_ROOT/steps/step-002/ours' '$REMOTE_ROOT/steps/step-002/rpki-client'"
scp "$ROOT_DIR/target/release/rpki" "${COPY_FILES[@]}" "$SSH_TARGET:$REMOTE_ROOT/"
if [[ "$PROBE_RPKI_CLIENT_CACHE" == "1" ]]; then
scp "$ROOT_DIR/target/release/cir_probe_rpki_client_cache" "$SSH_TARGET:$REMOTE_ROOT/bin/"
fi
scp "$RPKI_CLIENT_BIN" "$SSH_TARGET:$REMOTE_ROOT/bin/rpki-client"
scp "$LIBTLS_PATH" "$SSH_TARGET:$REMOTE_ROOT/lib/libtls.so.28"
printf '%s' "$OURS_EXTRA_ARGS" | ssh "$SSH_TARGET" "cat > '$REMOTE_ROOT/ours-extra-args.txt'"
printf '%s' "$RP_RUN_MODE" | ssh "$SSH_TARGET" "cat > '$REMOTE_ROOT/rp-run-mode.txt'"
printf '%s' "$RIR_SET" | ssh "$SSH_TARGET" "cat > '$REMOTE_ROOT/rir-set.txt'"
run_step() {
local step_id="$1"
local kind="$2"
local local_step="$RUN_ROOT/steps/$step_id"
ssh "$SSH_TARGET" bash -s -- "$REMOTE_ROOT" "$step_id" "$kind" <<'EOS'
set -euo pipefail
REMOTE_ROOT="$1"
STEP_ID="$2"
KIND="$3"
cd "$REMOTE_ROOT"
mkdir -p "steps/$STEP_ID/ours" "steps/$STEP_ID/rpki-client"
touch rpki-client-skiplist
chmod 0644 rpki-client-skiplist
OURS_EXTRA_ARGS="$(cat ours-extra-args.txt)"
RP_RUN_MODE="$(cat rp-run-mode.txt)"
RIR_SET="$(cat rir-set.txt)"
OURS_EXTRA_ARGV=()
if [[ -n "$OURS_EXTRA_ARGS" ]]; then
# shellcheck disable=SC2206
OURS_EXTRA_ARGV=($OURS_EXTRA_ARGS)
fi
case "$RIR_SET" in
mixed2) RIRS=(apnic arin) ;;
all5) RIRS=(afrinic apnic arin lacnic ripe) ;;
*) echo "invalid rir set: $RIR_SET" >&2; exit 2 ;;
esac
tal_file_for_rir() {
case "$1" in
afrinic) printf '%s' "afrinic.tal" ;;
apnic) printf '%s' "apnic-rfc7730-https.tal" ;;
arin) printf '%s' "arin.tal" ;;
lacnic) printf '%s' "lacnic.tal" ;;
ripe) printf '%s' "ripe-ncc.tal" ;;
*) echo "unknown rir: $1" >&2; exit 2 ;;
esac
}
tal_uri_for_rir() {
case "$1" in
afrinic) printf '%s' "https://rpki.afrinic.net/repository/AfriNIC.cer" ;;
apnic) printf '%s' "https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer" ;;
arin) printf '%s' "https://rrdp.arin.net/arin-rpki-ta.cer" ;;
lacnic) printf '%s' "https://rrdp.lacnic.net/ta/rta-lacnic-rpki.cer" ;;
ripe) printf '%s' "https://rpki.ripe.net/ta/ripe-ncc-ta.cer" ;;
*) echo "unknown rir: $1" >&2; exit 2 ;;
esac
}
ta_file_for_rir() {
case "$1" in
afrinic) printf '%s' "afrinic-ta.cer" ;;
apnic) printf '%s' "apnic-ta.cer" ;;
arin) printf '%s' "arin-ta.cer" ;;
lacnic) printf '%s' "lacnic-ta.cer" ;;
ripe) printf '%s' "ripe-ncc-ta.cer" ;;
*) echo "unknown rir: $1" >&2; exit 2 ;;
esac
}
refresh_ta_file_for_rir() {
local rir="$1"
local uri
local file
uri="$(tal_uri_for_rir "$rir")"
file="$(ta_file_for_rir "$rir")"
python3 - <<'PY' "$uri" "$file"
import sys
import urllib.request
uri, path = sys.argv[1:]
request = urllib.request.Request(uri, headers={"User-Agent": "rpki-dev/compare-fast-path"})
with urllib.request.urlopen(request, timeout=30) as response:
data = response.read()
if not data:
raise SystemExit(f"empty TA certificate response: {uri}")
with open(path, "wb") as output:
output.write(data)
PY
}
for rir in "${RIRS[@]}"; do
refresh_ta_file_for_rir "$rir"
done
OURS_TAL_ARGS=()
CLIENT_TAL_ARGS=()
OURS_CIR_TAL_ARGS=()
for rir in "${RIRS[@]}"; do
tal_file="$(tal_file_for_rir "$rir")"
ta_file="$(ta_file_for_rir "$rir")"
tal_uri="$(tal_uri_for_rir "$rir")"
OURS_TAL_ARGS+=(--tal-path "$tal_file" --ta-path "$ta_file")
OURS_CIR_TAL_ARGS+=(--cir-tal-uri "$tal_uri")
CLIENT_TAL_ARGS+=(-t "../../$tal_file")
done
if [[ "$KIND" == "snapshot" ]]; then
rm -rf state/ours/work-db state/ours/raw-store.db state/ours/repo-bytes.db state/rpki-client/cache state/rpki-client/out state/rpki-client/ta state/rpki-client/.ta
fi
mkdir -p state/ours/work-db state/ours/raw-store.db state/ours/repo-bytes.db state/rpki-client/cache state/rpki-client/out state/rpki-client/ta state/rpki-client/.ta
chmod 0777 state/ours/work-db state/ours/raw-store.db state/ours/repo-bytes.db
chmod -R 0777 state/rpki-client
touch state/rpki-client/rpki-client-skiplist
chmod 0644 state/rpki-client/rpki-client-skiplist
START_EPOCH="$(python3 - <<'PY'
import time
print(time.time() + 3.0)
PY
)"
run_ours() {
python3 - <<'PY' "$START_EPOCH"
import sys, time
x = float(sys.argv[1])
d = x - time.time()
if d > 0:
time.sleep(d)
PY
started_ms="$(python3 - <<'PY'
import time
print(int(time.time() * 1000))
PY
)"
set +e
env RPKI_PROGRESS_LOG=1 RPKI_PROGRESS_SLOW_SECS=0 ./rpki \
--db state/ours/work-db \
--raw-store-db state/ours/raw-store.db \
--repo-bytes-db state/ours/repo-bytes.db \
"${OURS_TAL_ARGS[@]}" \
"${OURS_EXTRA_ARGV[@]}" \
--ccr-out "steps/$STEP_ID/ours/result.ccr" \
--cir-enable \
--cir-out "steps/$STEP_ID/ours/result.cir" \
"${OURS_CIR_TAL_ARGS[@]}" \
--report-json "steps/$STEP_ID/ours/report.json" \
> "steps/$STEP_ID/ours/run.log" 2>&1
exit_code=$?
set -e
finished_ms="$(python3 - <<'PY'
import time
print(int(time.time() * 1000))
PY
)"
python3 - <<'PY' "steps/$STEP_ID/ours/round-result.json" "$STEP_ID" "$KIND" "$started_ms" "$finished_ms" "$exit_code"
import json, sys
path, step_id, kind, started_ms, finished_ms, exit_code = sys.argv[1:]
json.dump(
{
"stepId": step_id,
"kind": kind,
"durationMs": int(finished_ms) - int(started_ms),
"exitCode": int(exit_code),
},
open(path, "w"),
indent=2,
)
PY
}
run_client() {
cd state/rpki-client
python3 - <<'PY' "$START_EPOCH"
import sys, time
x = float(sys.argv[1])
d = x - time.time()
if d > 0:
time.sleep(d)
PY
started_ms="$(python3 - <<'PY'
import time
print(int(time.time() * 1000))
PY
)"
set +e
LD_LIBRARY_PATH="$REMOTE_ROOT/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" "$REMOTE_ROOT/bin/rpki-client" \
-vv \
-S rpki-client-skiplist \
"${CLIENT_TAL_ARGS[@]}" \
-d cache out \
> "$REMOTE_ROOT/steps/$STEP_ID/rpki-client/run.log" 2>&1
exit_code=$?
set -e
cp out/rpki.ccr "$REMOTE_ROOT/steps/$STEP_ID/rpki-client/result.ccr" 2>/dev/null || true
cp out/rpki.cir "$REMOTE_ROOT/steps/$STEP_ID/rpki-client/result.cir" 2>/dev/null || true
cp out/openbgpd "$REMOTE_ROOT/steps/$STEP_ID/rpki-client/openbgpd" 2>/dev/null || true
finished_ms="$(python3 - <<'PY'
import time
print(int(time.time() * 1000))
PY
)"
python3 - <<'PY' "$REMOTE_ROOT/steps/$STEP_ID/rpki-client/round-result.json" "$STEP_ID" "$KIND" "$started_ms" "$finished_ms" "$exit_code"
import json, sys
path, step_id, kind, started_ms, finished_ms, exit_code = sys.argv[1:]
json.dump(
{
"stepId": step_id,
"kind": kind,
"durationMs": int(finished_ms) - int(started_ms),
"exitCode": int(exit_code),
},
open(path, "w"),
indent=2,
)
PY
}
if [[ "$RP_RUN_MODE" == "parallel" ]]; then
run_ours &
OURS_PID=$!
run_client &
CLIENT_PID=$!
wait "$OURS_PID"
wait "$CLIENT_PID"
else
run_ours
run_client
fi
EOS
for rel in result.ccr result.cir round-result.json run.log stage-timing.json; do
scp -C "$SSH_TARGET:$REMOTE_ROOT/steps/$step_id/ours/$rel" "$local_step/ours/"
done
for rel in result.ccr result.cir round-result.json run.log openbgpd; do
scp -C "$SSH_TARGET:$REMOTE_ROOT/steps/$step_id/rpki-client/$rel" "$local_step/rpki-client/" || true
done
if [[ "$COPY_RPKI_CLIENT_CACHE" == "1" ]]; then
mkdir -p "$local_step/rpki-client/cache"
rsync -a --delete "$SSH_TARGET:$REMOTE_ROOT/state/rpki-client/cache/" "$local_step/rpki-client/cache/"
fi
if [[ -f "$local_step/ours/result.cir" && -f "$local_step/rpki-client/result.cir" ]]; then
"$ROOT_DIR/scripts/periodic/compare_ccr_cir_round.sh" \
--ours-ccr "$local_step/ours/result.ccr" \
--rpki-client-ccr "$local_step/rpki-client/result.ccr" \
--ours-cir "$local_step/ours/result.cir" \
--rpki-client-cir "$local_step/rpki-client/result.cir" \
--out-dir "$local_step/compare" \
--trust-anchor unknown >/dev/null
if [[ "$PROBE_RPKI_CLIENT_CACHE" == "1" ]]; then
ssh "$SSH_TARGET" "set -e; mkdir -p '$REMOTE_ROOT/steps/$step_id/compare/cir'; '$REMOTE_ROOT/bin/cir_probe_rpki_client_cache' --ours-cir '$REMOTE_ROOT/steps/$step_id/ours/result.cir' --rpki-client-cir '$REMOTE_ROOT/steps/$step_id/rpki-client/result.cir' --cache-root '$REMOTE_ROOT/state/rpki-client/cache' --rpki-client-log '$REMOTE_ROOT/steps/$step_id/rpki-client/run.log' --out-json '$REMOTE_ROOT/steps/$step_id/compare/cir/rpki-client-cache-probe.json' --sample-limit 50 >/dev/null"
scp -C "$SSH_TARGET:$REMOTE_ROOT/steps/$step_id/compare/cir/rpki-client-cache-probe.json" "$local_step/compare/cir/"
fi
if [[ "$COPY_RPKI_CLIENT_CACHE" == "1" ]]; then
"$ROOT_DIR/target/release/cir_probe_rpki_client_cache" \
--ours-cir "$local_step/ours/result.cir" \
--rpki-client-cir "$local_step/rpki-client/result.cir" \
--cache-root "$local_step/rpki-client/cache" \
--rpki-client-log "$local_step/rpki-client/run.log" \
--out-json "$local_step/compare/cir/rpki-client-cache-probe.json" \
--sample-limit 50 >/dev/null
fi
else
"$ROOT_DIR/scripts/periodic/compare_ccr_round.sh" \
--ours-ccr "$local_step/ours/result.ccr" \
--rpki-client-ccr "$local_step/rpki-client/result.ccr" \
--out-dir "$local_step/compare" \
--trust-anchor unknown >/dev/null
fi
python3 - <<'PY' "$local_step/ours/round-result.json" "$local_step/rpki-client/round-result.json" "$local_step/ours/stage-timing.json" "$local_step/compare/summary.json" "$local_step/compare/compare-summary.json" "$local_step/step-summary.json" "$OURS_EXTRA_ARGS"
import json, sys
ours = json.load(open(sys.argv[1]))
client = json.load(open(sys.argv[2]))
stage = json.load(open(sys.argv[3]))
compare_path = sys.argv[4] if __import__('pathlib').Path(sys.argv[4]).exists() else sys.argv[5]
compare = json.load(open(compare_path))
ours_extra_args = sys.argv[7]
json.dump(
{
"stepId": ours["stepId"],
"kind": ours["kind"],
"oursExtraArgs": ours_extra_args,
"oursDurationMs": ours["durationMs"],
"rpkiClientDurationMs": client["durationMs"],
"oursExitCode": ours["exitCode"],
"rpkiClientExitCode": client["exitCode"],
"oursTotalMs": stage["total_ms"],
"oursRepoSyncMsTotal": stage["repo_sync_ms_total"],
"oursPublicationPointRepoSyncMsTotal": stage.get("publication_point_repo_sync_ms_total"),
"oursDownloadEventCount": stage.get("download_event_count"),
"oursRrdpDownloadMsTotal": stage.get("rrdp_download_ms_total"),
"oursRsyncDownloadMsTotal": stage.get("rsync_download_ms_total"),
"oursDownloadBytesTotal": stage.get("download_bytes_total"),
"oursVrps": compare["vrps"]["ours"],
"rpkiClientVrps": compare["vrps"]["rpkiClient"],
"oursVaps": compare["vaps"]["ours"],
"rpkiClientVaps": compare["vaps"]["rpkiClient"],
"vrpMatch": compare["vrps"]["match"],
"vapMatch": compare["vaps"]["match"],
"allMatch": compare["allMatch"],
"onlyInOurs": len(compare["vrps"]["onlyInOurs"]),
"onlyInRpkiClient": len(compare["vrps"]["onlyInRpkiClient"]),
},
open(sys.argv[6], "w"),
indent=2,
)
PY
}
run_step step-001 snapshot
run_step step-002 delta
python3 - <<'PY' "$RUN_ROOT/steps/step-001/step-summary.json" "$RUN_ROOT/steps/step-002/step-summary.json" "$RUN_ROOT/summary.json" "$RP_RUN_MODE" "$OURS_EXTRA_ARGS" "$RIR_SET" "$SCOPE_LABEL" "${RIRS[@]}"
import json, sys
steps = [json.load(open(p)) for p in sys.argv[1:3]]
summary = {
"workflowName": "性能对比测试快速版",
"scope": sys.argv[7],
"rpRunMode": sys.argv[4],
"oursExtraArgs": sys.argv[5],
"rirSet": sys.argv[6],
"rirs": sys.argv[8:],
"steps": steps,
}
json.dump(summary, open(sys.argv[3], "w"), indent=2, ensure_ascii=False)
print(json.dumps(summary, indent=2, ensure_ascii=False))
PY

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,19 @@ set -euo pipefail
# rustup component add llvm-tools-preview
# cargo install cargo-llvm-cov --locked
# Optional:
# COVERAGE_FORCE_CLEAN=1 Force `cargo llvm-cov clean --workspace` before the run.
# Default behavior is to reuse existing llvm-cov build artifacts.
# RPKI_SKIP_HEAVY_SCRIPT_REPLAY_TESTS=1 Skip replay/matrix integration tests that
# spawn shell pipelines and can trigger separate release builds.
# coverage.sh enables this by default.
# RPKI_SKIP_HEAVY_BLACKBOX_TESTS=1 Skip slower blackbox CLI/script integration tests
# that provide low incremental coverage per wall-clock second.
# coverage.sh enables this by default.
# RPKI_SKIP_HEAVY_CRYPTO_TESTS=1 Skip slower OpenSSL-heavy certificate generation tests
# that provide low incremental coverage per wall-clock second.
# coverage.sh enables this by default.
run_out="$(mktemp)"
text_out="$(mktemp)"
html_out="$(mktemp)"
@@ -14,13 +27,22 @@ cleanup() {
}
trap cleanup EXIT
IGNORE_REGEX='src/bin/replay_bundle_capture\.rs|src/bin/replay_bundle_capture_delta\.rs|src/bin/replay_bundle_capture_sequence\.rs|src/bin/replay_bundle_record\.rs|src/bin/replay_bundle_refresh_sequence_outputs\.rs|src/bin/measure_sequence_replay\.rs|src/bin/repository_view_stats\.rs|src/bin/trace_arin_missing_vrps\.rs|src/bin/db_stats\.rs|src/bin/rrdp_state_dump\.rs|src/bin/ccr_dump\.rs|src/bin/ccr_verify\.rs|src/bin/ccr_to_routinator_csv\.rs|src/bin/ccr_to_compare_views\.rs|src/bin/cir_materialize\.rs|src/bin/cir_extract_inputs\.rs|src/bin/cir_drop_report\.rs|src/bin/cir_ta_only_fixture\.rs|src/bundle/live_capture\.rs|src/bundle/record_io\.rs|src/bundle/compare_view\.rs|src/progress_log\.rs|src/cli\.rs|src/validation/run_tree_from_tal\.rs|src/validation/from_tal\.rs|src/sync/store_projection\.rs|src/cir/materialize\.rs'
IGNORE_REGEX='repository_view_stats\.rs|db_stats\.rs|rrdp_state_dump\.rs|ccr_dump\.rs|ccr_verify\.rs|ccr_to_routinator_csv\.rs|ccr_to_compare_views\.rs|cir_materialize\.rs|cir_extract_inputs\.rs|cir_drop_report\.rs|cir_ta_only_fixture\.rs|cir_dump_reject_list\.rs|rpki_object_parse\.rs|rpki_query_indexer\.rs|rpki_query_service\.rs|triage_ccr_cir_pair\.rs|rpki_artifact_metrics|rpki_inter_rp_metrics|rpki_daemon\.rs|sequence_triage_ccr_cir|ccr_state_compare\.rs|cir_state_compare\.rs|cir_probe_rpki_client_cache\.rs|ccr/compare_view\.rs|progress_log\.rs|cli\.rs|validation/run_tree_from_tal\.rs|validation/tree_parallel\.rs|validation/tree_runner|validation/from_tal\.rs|sync/store_projection\.rs|sync/repo\.rs|sync/rrdp|(^|/)storage(/|\.rs$)|cir/materialize\.rs'
# Preserve colored output even though we post-process output by running under a pseudo-TTY.
# We run tests only once, then generate both CLI text + HTML reports without rerunning tests.
set +e
cargo llvm-cov clean --workspace >/dev/null 2>&1
if [ "${COVERAGE_FORCE_CLEAN:-0}" = "1" ]; then
cargo llvm-cov clean --workspace >/dev/null 2>&1
echo "coverage mode: clean build (COVERAGE_FORCE_CLEAN=1)"
else
echo "coverage mode: reuse existing llvm-cov artifacts (default)"
fi
export RPKI_SKIP_HEAVY_SCRIPT_REPLAY_TESTS="${RPKI_SKIP_HEAVY_SCRIPT_REPLAY_TESTS:-1}"
export RPKI_SKIP_HEAVY_BLACKBOX_TESTS="${RPKI_SKIP_HEAVY_BLACKBOX_TESTS:-1}"
export RPKI_SKIP_HEAVY_CRYPTO_TESTS="${RPKI_SKIP_HEAVY_CRYPTO_TESTS:-1}"
# 1) Run tests once to collect coverage data (no report).
script -q -e -c "CARGO_TERM_COLOR=always cargo llvm-cov --no-report" "$run_out" >/dev/null 2>&1

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "$SCRIPT_DIR/build_docker_installer_package.sh" --arch arm64 "$@"

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "$SCRIPT_DIR/build_docker_metrics_image.sh" --arch arm64 "$@"

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "$SCRIPT_DIR/build_docker_runtime_image.sh" --arch arm64 "$@"

View File

@@ -0,0 +1,465 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
TARGET_ARCH="${TARGET_ARCH:-}"
IMAGE_TAG=""
IMAGE_TAR=""
METRICS_IMAGE=""
METRICS_IMAGE_TAR=""
PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-prom/prometheus:v2.55.1}"
PROMETHEUS_IMAGE_TAR="${PROMETHEUS_IMAGE_TAR:-}"
GRAFANA_IMAGE="${GRAFANA_IMAGE:-grafana/grafana:11.3.1}"
GRAFANA_IMAGE_TAR="${GRAFANA_IMAGE_TAR:-}"
OUT_DIR="${OUT_DIR:-}"
PACKAGE_PREFIX="${PACKAGE_PREFIX:-}"
TEMPLATE_DIR="${TEMPLATE_DIR:-$REPO_ROOT/deploy/docker-installer}"
ALLOW_DIRTY=0
usage() {
cat <<'USAGE'
Usage:
scripts/docker/build_docker_installer_package.sh --arch amd64|arm64 [options]
Options:
--arch <arch> Target architecture: amd64|arm64
--prometheus-image <tag>
Prometheus image tag to record and package.
--prometheus-image-tar <path>
Existing Prometheus docker save tar/tar.gz to include.
--grafana-image <tag>
Grafana image tag to record and package.
--grafana-image-tar <path>
Existing Grafana docker save tar/tar.gz to include.
--out-dir <path> Output directory.
--prefix <name> Package directory/tar prefix.
--template-dir <path>
Package template directory.
--allow-dirty Allow a development build from a dirty worktree and mark
image tags, package name and manifest as dirty.
-h, --help Show help.
Every package rebuilds runtime and metrics images from the current HEAD. Their
tags are ours-rp-runtime-<arch>:<git8> and ours-rp-metrics-<arch>:<git8>.
External runtime/metrics image tags and tar archives are intentionally rejected.
USAGE
}
normalize_arch() {
case "$1" in
amd64|x86_64)
printf 'amd64\n'
;;
arm64|aarch64)
printf 'arm64\n'
;;
*)
return 1
;;
esac
}
platform_for_arch() {
printf 'linux/%s\n' "$1"
}
safe_tag_name() {
printf '%s' "$1" | tr '/:' '--'
}
runtime_image_tag() {
local arch="$1"
local revision="$2"
printf 'ours-rp-runtime-%s:%s\n' "$arch" "$revision"
}
metrics_image_tag() {
local arch="$1"
local revision="$2"
printf 'ours-rp-metrics-%s:%s\n' "$arch" "$revision"
}
default_package_prefix() {
printf 'ours-rp-installer-%s\n' "$1"
}
default_host_data_dir() {
printf '/var/lib/ours-rp-%s-installer\n' "$1"
}
default_compose_project() {
printf 'ours-rp-%s-installer\n' "$1"
}
default_metrics_instance() {
printf '%s-installer\n' "$1"
}
replace_or_append_env() {
local env_path="$1"
local key="$2"
local value="$3"
local tmp_env
tmp_env="${env_path}.tmp"
awk -v key="$key" -v value="$value" '
BEGIN { done=0 }
$0 ~ "^" key "=" { print key "=" value; done=1; next }
{ print }
END { if (!done) print key "=" value }
' "$env_path" > "$tmp_env"
mv "$tmp_env" "$env_path"
}
replace_text_in_file() {
local file_path="$1"
local old_text="$2"
local new_text="$3"
OLD_TEXT="$old_text" NEW_TEXT="$new_text" perl -0pi -e 's/\Q$ENV{OLD_TEXT}\E/$ENV{NEW_TEXT}/g' "$file_path"
}
ensure_image_for_platform() {
local image="$1"
local expected_platform="$2"
local role="$3"
local actual_platform
if ! docker image inspect "$image" >/dev/null 2>&1; then
echo "pulling $role image for $expected_platform: $image" >&2
docker pull --platform "$expected_platform" "$image" >&2
fi
actual_platform="$(docker image inspect --format '{{.Os}}/{{.Architecture}}' "$image" 2>/dev/null || echo unknown)"
if [[ "$actual_platform" != "$expected_platform" ]]; then
echo "re-pulling $role image for $expected_platform: $image (current=$actual_platform)" >&2
docker pull --platform "$expected_platform" "$image" >&2
actual_platform="$(docker image inspect --format '{{.Os}}/{{.Architecture}}' "$image" 2>/dev/null || echo unknown)"
fi
[[ "$actual_platform" == "$expected_platform" ]] || {
cat >&2 <<EOF
wrong platform for $role image: $image
expected: $expected_platform
actual: $actual_platform
EOF
exit 2
}
}
save_image_if_needed() {
local image="$1"
local existing_tar="$2"
local out_dir="$3"
local role="$4"
local expected_platform="$5"
if [[ -n "$existing_tar" ]]; then
[[ -f "$existing_tar" ]] || {
echo "missing $role image tar: $existing_tar" >&2
exit 2
}
printf '%s\n' "$existing_tar"
return 0
fi
ensure_image_for_platform "$image" "$expected_platform" "$role"
local tar_path="$out_dir/$(safe_tag_name "$image")-${TARGET_ARCH}.tar.gz"
echo "saving $role image to $tar_path" >&2
docker save "$image" | gzip -c > "$tar_path"
printf '%s\n' "$tar_path"
}
image_label() {
local image="$1"
local key="$2"
docker image inspect --format "{{ index .Config.Labels \"$key\" }}" "$image"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--arch)
TARGET_ARCH="$(normalize_arch "$2")" || {
echo "unsupported target architecture: $2" >&2
exit 2
}
shift 2
;;
--image|--image-tar|--metrics-image|--metrics-image-tar)
echo "$1 is no longer supported: installer packages always rebuild runtime and metrics images from current HEAD" >&2
exit 2
;;
--prometheus-image)
PROMETHEUS_IMAGE="$2"
shift 2
;;
--prometheus-image-tar)
PROMETHEUS_IMAGE_TAR="$2"
shift 2
;;
--grafana-image)
GRAFANA_IMAGE="$2"
shift 2
;;
--grafana-image-tar)
GRAFANA_IMAGE_TAR="$2"
shift 2
;;
--out-dir)
OUT_DIR="$2"
shift 2
;;
--prefix)
PACKAGE_PREFIX="$2"
shift 2
;;
--template-dir)
TEMPLATE_DIR="$2"
shift 2
;;
--allow-dirty)
ALLOW_DIRTY=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
[[ -n "$TARGET_ARCH" ]] || {
echo "--arch is required" >&2
usage >&2
exit 2
}
[[ -d "$TEMPLATE_DIR" ]] || {
echo "missing template dir: $TEMPLATE_DIR" >&2
exit 2
}
SOURCE_COMMIT_FULL="$(git -C "$REPO_ROOT" rev-parse HEAD)"
SOURCE_COMMIT_SHORT="$(git -C "$REPO_ROOT" rev-parse --short=8 HEAD)"
SOURCE_DIRTY="false"
if [[ -n "$(git -C "$REPO_ROOT" status --short)" ]]; then
SOURCE_DIRTY="true"
fi
if [[ "$SOURCE_DIRTY" == "true" && "$ALLOW_DIRTY" != "1" ]]; then
cat >&2 <<EOF
refusing to build a release installer from a dirty worktree.
source_commit: $SOURCE_COMMIT_FULL
Use --allow-dirty only for development verification; the package and image tags
will be explicitly marked dirty.
EOF
exit 2
fi
TIMESTAMP="$(date -u +%Y%m%dT%H%M%SZ)"
REVISION_TOKEN="$SOURCE_COMMIT_SHORT"
if [[ "$SOURCE_DIRTY" == "true" ]]; then
REVISION_TOKEN="${REVISION_TOKEN}-dirty"
fi
IMAGE_TAG="$(runtime_image_tag "$TARGET_ARCH" "$REVISION_TOKEN")"
METRICS_IMAGE="$(metrics_image_tag "$TARGET_ARCH" "$REVISION_TOKEN")"
if [[ -z "$OUT_DIR" ]]; then
OUT_DIR="$REPO_ROOT/target/${TARGET_ARCH}-installer"
fi
if [[ -z "$PACKAGE_PREFIX" ]]; then
PACKAGE_PREFIX="$(default_package_prefix "$TARGET_ARCH")"
fi
TARGET_PLATFORM="$(platform_for_arch "$TARGET_ARCH")"
HOST_DATA_DIR_DEFAULT="$(default_host_data_dir "$TARGET_ARCH")"
COMPOSE_PROJECT_DEFAULT="$(default_compose_project "$TARGET_ARCH")"
METRICS_INSTANCE_DEFAULT="$(default_metrics_instance "$TARGET_ARCH")"
IMAGE_OUT_DIR="$REPO_ROOT/target/${TARGET_ARCH}-docker"
mkdir -p "$OUT_DIR"
echo "rebuilding runtime image: $IMAGE_TAG"
"$SCRIPT_DIR/build_docker_runtime_image.sh" \
--arch "$TARGET_ARCH" \
--image "$IMAGE_TAG" \
--out-dir "$IMAGE_OUT_DIR"
echo "rebuilding metrics image: $METRICS_IMAGE"
"$SCRIPT_DIR/build_docker_metrics_image.sh" \
--arch "$TARGET_ARCH" \
--image "$METRICS_IMAGE" \
--out-dir "$IMAGE_OUT_DIR"
IMAGE_TAR="$IMAGE_OUT_DIR/$(safe_tag_name "$IMAGE_TAG").tar.gz"
METRICS_IMAGE_TAR="$IMAGE_OUT_DIR/$(safe_tag_name "$METRICS_IMAGE").tar.gz"
[[ -f "$IMAGE_TAR" ]] || { echo "runtime image build did not produce $IMAGE_TAR" >&2; exit 2; }
[[ -f "$METRICS_IMAGE_TAR" ]] || { echo "metrics image build did not produce $METRICS_IMAGE_TAR" >&2; exit 2; }
runtime_image_revision="$(image_label "$IMAGE_TAG" "org.opencontainers.image.revision")"
metrics_image_revision="$(image_label "$METRICS_IMAGE" "org.opencontainers.image.revision")"
runtime_image_dirty="$(image_label "$IMAGE_TAG" "org.opencontainers.image.source-dirty")"
metrics_image_dirty="$(image_label "$METRICS_IMAGE" "org.opencontainers.image.source-dirty")"
[[ "$runtime_image_revision" == "$SOURCE_COMMIT_FULL" ]] || { echo "runtime image revision mismatch: $runtime_image_revision != $SOURCE_COMMIT_FULL" >&2; exit 2; }
[[ "$metrics_image_revision" == "$SOURCE_COMMIT_FULL" ]] || { echo "metrics image revision mismatch: $metrics_image_revision != $SOURCE_COMMIT_FULL" >&2; exit 2; }
[[ "$runtime_image_dirty" == "$SOURCE_DIRTY" ]] || { echo "runtime image dirty flag mismatch" >&2; exit 2; }
[[ "$metrics_image_dirty" == "$SOURCE_DIRTY" ]] || { echo "metrics image dirty flag mismatch" >&2; exit 2; }
package_name="${PACKAGE_PREFIX}-${REVISION_TOKEN}-${TIMESTAMP}"
stage="$OUT_DIR/$package_name"
tar_path="$OUT_DIR/$package_name.tar.gz"
rm -rf "$stage"
rsync -a --delete "$TEMPLATE_DIR"/ "$stage"/
mkdir -p "$stage/images"
cp "$IMAGE_TAR" "$stage/images/"
cp "$METRICS_IMAGE_TAR" "$stage/images/"
monitor_image_stage="$OUT_DIR/.monitor-images-$TARGET_ARCH-$TIMESTAMP"
rm -rf "$monitor_image_stage"
mkdir -p "$monitor_image_stage"
prometheus_tar="$(save_image_if_needed "$PROMETHEUS_IMAGE" "$PROMETHEUS_IMAGE_TAR" "$monitor_image_stage" "prometheus" "$TARGET_PLATFORM")"
grafana_tar="$(save_image_if_needed "$GRAFANA_IMAGE" "$GRAFANA_IMAGE_TAR" "$monitor_image_stage" "grafana" "$TARGET_PLATFORM")"
cp "$prometheus_tar" "$stage/images/"
cp "$grafana_tar" "$stage/images/"
if [[ -f "$stage/.env.example" ]]; then
replace_or_append_env "$stage/.env.example" "PACKAGE_ARCH" "$TARGET_ARCH"
replace_or_append_env "$stage/.env.example" "PACKAGE_PLATFORM" "$TARGET_PLATFORM"
replace_or_append_env "$stage/.env.example" "COMPOSE_PROJECT_NAME" "$COMPOSE_PROJECT_DEFAULT"
replace_or_append_env "$stage/.env.example" "RPKI_IMAGE" "$IMAGE_TAG"
replace_or_append_env "$stage/.env.example" "RPKI_PLATFORM" "$TARGET_PLATFORM"
replace_or_append_env "$stage/.env.example" "METRICS_IMAGE" "$METRICS_IMAGE"
replace_or_append_env "$stage/.env.example" "METRICS_PLATFORM" "$TARGET_PLATFORM"
replace_or_append_env "$stage/.env.example" "HOST_DATA_DIR" "$HOST_DATA_DIR_DEFAULT"
replace_or_append_env "$stage/.env.example" "RTR_REPORT_DIR" "$HOST_DATA_DIR_DEFAULT/empty-rtr-report"
replace_or_append_env "$stage/.env.example" "RTR_REPORT_CONTAINER_DIR" "/var/lib/ours-rp/rtr-report"
replace_or_append_env "$stage/.env.example" "ALLOW_CROSS_ARCH" "0"
replace_or_append_env "$stage/.env.example" "METRICS_INSTANCE" "$METRICS_INSTANCE_DEFAULT"
replace_or_append_env "$stage/.env.example" "MONITOR_PLATFORM" "$TARGET_PLATFORM"
replace_or_append_env "$stage/.env.example" "PROMETHEUS_IMAGE" "$PROMETHEUS_IMAGE"
replace_or_append_env "$stage/.env.example" "GRAFANA_IMAGE" "$GRAFANA_IMAGE"
fi
replace_text_in_file "$stage/docs/README.zh-CN.md" "__PACKAGE_ARCH__" "$TARGET_ARCH"
replace_text_in_file "$stage/docs/README.en.md" "__PACKAGE_ARCH__" "$TARGET_ARCH"
replace_text_in_file "$stage/docs/operations.zh-CN.md" "__PACKAGE_ARCH__" "$TARGET_ARCH"
replace_text_in_file "$stage/docs/operations.en.md" "__PACKAGE_ARCH__" "$TARGET_ARCH"
replace_text_in_file "$stage/docs/troubleshooting.zh-CN.md" "__PACKAGE_ARCH__" "$TARGET_ARCH"
replace_text_in_file "$stage/docs/troubleshooting.en.md" "__PACKAGE_ARCH__" "$TARGET_ARCH"
replace_text_in_file "$stage/docs/README.zh-CN.md" "__PACKAGE_PLATFORM__" "$TARGET_PLATFORM"
replace_text_in_file "$stage/docs/README.en.md" "__PACKAGE_PLATFORM__" "$TARGET_PLATFORM"
replace_text_in_file "$stage/docs/troubleshooting.zh-CN.md" "__PACKAGE_PLATFORM__" "$TARGET_PLATFORM"
replace_text_in_file "$stage/docs/troubleshooting.en.md" "__PACKAGE_PLATFORM__" "$TARGET_PLATFORM"
replace_text_in_file "$stage/docs/README.zh-CN.md" "__HOST_DATA_DIR__" "$HOST_DATA_DIR_DEFAULT"
replace_text_in_file "$stage/docs/README.en.md" "__HOST_DATA_DIR__" "$HOST_DATA_DIR_DEFAULT"
replace_text_in_file "$stage/docs/operations.zh-CN.md" "__HOST_DATA_DIR__" "$HOST_DATA_DIR_DEFAULT"
replace_text_in_file "$stage/docs/operations.en.md" "__HOST_DATA_DIR__" "$HOST_DATA_DIR_DEFAULT"
replace_text_in_file "$stage/docs/troubleshooting.zh-CN.md" "__HOST_DATA_DIR__" "$HOST_DATA_DIR_DEFAULT"
replace_text_in_file "$stage/docs/troubleshooting.en.md" "__HOST_DATA_DIR__" "$HOST_DATA_DIR_DEFAULT"
replace_text_in_file "$stage/docs/README.zh-CN.md" "__RUNTIME_IMAGE__" "$IMAGE_TAG"
replace_text_in_file "$stage/docs/README.en.md" "__RUNTIME_IMAGE__" "$IMAGE_TAG"
replace_text_in_file "$stage/docs/README.zh-CN.md" "__METRICS_IMAGE__" "$METRICS_IMAGE"
replace_text_in_file "$stage/docs/README.en.md" "__METRICS_IMAGE__" "$METRICS_IMAGE"
runtime_tar_sha256="$(sha256sum "$IMAGE_TAR" | awk '{print $1}')"
metrics_tar_sha256="$(sha256sum "$METRICS_IMAGE_TAR" | awk '{print $1}')"
prometheus_tar_sha256="$(sha256sum "$prometheus_tar" | awk '{print $1}')"
grafana_tar_sha256="$(sha256sum "$grafana_tar" | awk '{print $1}')"
cat > "$stage/PACKAGE-MANIFEST.env" <<EOF
package_schema_version=2
package_name=$package_name
created_at_utc=$TIMESTAMP
build_timestamp_utc=$TIMESTAMP
source_commit=$SOURCE_COMMIT_FULL
source_commit_short=$SOURCE_COMMIT_SHORT
source_dirty=$SOURCE_DIRTY
git_commit=$SOURCE_COMMIT_FULL
git_status_count=$(git -C "$REPO_ROOT" status --short 2>/dev/null | wc -l | tr -d ' ')
PACKAGE_ARCH=$TARGET_ARCH
PACKAGE_PLATFORM=$TARGET_PLATFORM
package_arch=$TARGET_ARCH
package_platform=$TARGET_PLATFORM
cross_arch_default=deny
cross_arch_enable_var=ALLOW_CROSS_ARCH
RPKI_IMAGE=$IMAGE_TAG
RPKI_PLATFORM=$TARGET_PLATFORM
image_tag=$IMAGE_TAG
image_tar=$(basename "$IMAGE_TAR")
image_tar_size_bytes=$(wc -c < "$IMAGE_TAR")
image_tar_sha256=$runtime_tar_sha256
runtime_image=$IMAGE_TAG
runtime_image_revision=$runtime_image_revision
runtime_image_dirty=$runtime_image_dirty
rpki_image=$IMAGE_TAG
rpki_platform=$TARGET_PLATFORM
METRICS_IMAGE=$METRICS_IMAGE
METRICS_PLATFORM=$TARGET_PLATFORM
metrics_image=$METRICS_IMAGE
metrics_image_tar=$(basename "$METRICS_IMAGE_TAR")
metrics_image_tar_size_bytes=$(wc -c < "$METRICS_IMAGE_TAR")
metrics_image_tar_sha256=$metrics_tar_sha256
metrics_image_revision=$metrics_image_revision
metrics_image_dirty=$metrics_image_dirty
PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE
prometheus_image=$PROMETHEUS_IMAGE
prometheus_image_tar=$(basename "$prometheus_tar")
prometheus_image_tar_size_bytes=$(wc -c < "$prometheus_tar")
prometheus_image_tar_sha256=$prometheus_tar_sha256
GRAFANA_IMAGE=$GRAFANA_IMAGE
grafana_image=$GRAFANA_IMAGE
grafana_image_tar=$(basename "$grafana_tar")
grafana_image_tar_size_bytes=$(wc -c < "$grafana_tar")
grafana_image_tar_sha256=$grafana_tar_sha256
target_platform=$TARGET_PLATFORM
MONITOR_PLATFORM=$TARGET_PLATFORM
rpki_image_tar=$(basename "$IMAGE_TAR")
monitor_platform=$TARGET_PLATFORM
metrics_platform=$TARGET_PLATFORM
default_host_data_dir=$HOST_DATA_DIR_DEFAULT
default_compose_project_name=$COMPOSE_PROJECT_DEFAULT
default_metrics_instance=$METRICS_INSTANCE_DEFAULT
EOF
cat > "$stage/PACKAGE-SUMMARY.txt" <<EOF
package_name: $package_name
source_commit: $SOURCE_COMMIT_FULL
source_commit_short: $SOURCE_COMMIT_SHORT
source_dirty: $SOURCE_DIRTY
build_timestamp_utc: $TIMESTAMP
package_arch: $TARGET_ARCH
package_platform: $TARGET_PLATFORM
runtime_image: $IMAGE_TAG
runtime_image_revision: $runtime_image_revision
metrics_image: $METRICS_IMAGE
metrics_image_revision: $metrics_image_revision
prometheus_image: $PROMETHEUS_IMAGE
grafana_image: $GRAFANA_IMAGE
default_host_data_dir: $HOST_DATA_DIR_DEFAULT
default_compose_project_name: $COMPOSE_PROJECT_DEFAULT
cross_arch_default: deny
cross_arch_enable_var: ALLOW_CROSS_ARCH
EOF
chmod +x "$stage/scripts"/*.sh
if find "$stage" -maxdepth 1 -type f -name '*.sh' -print -quit | grep -q .; then
echo "error: operational scripts must be installed under scripts/ only" >&2
exit 1
fi
tar -C "$OUT_DIR" -czf "$tar_path" "$package_name"
package_sha256="$(sha256sum "$tar_path" | awk '{print $1}')"
rm -rf "$monitor_image_stage"
{
echo "package=$tar_path"
echo "package_dir=$stage"
echo "package_size_bytes=$(wc -c < "$tar_path")"
echo "package_sha256=$package_sha256"
echo "source_commit=$SOURCE_COMMIT_FULL"
echo "source_commit_short=$SOURCE_COMMIT_SHORT"
echo "source_dirty=$SOURCE_DIRTY"
echo "build_timestamp_utc=$TIMESTAMP"
echo "package_arch=$TARGET_ARCH"
echo "package_platform=$TARGET_PLATFORM"
echo "manifest=$stage/PACKAGE-MANIFEST.env"
echo "summary=$stage/PACKAGE-SUMMARY.txt"
} > "$OUT_DIR/$package_name.summary.env"
echo "package built: $tar_path"

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
normalize_arch() {
case "$1" in
amd64|x86_64)
printf 'amd64\n'
;;
arm64|aarch64)
printf 'arm64\n'
;;
*)
return 1
;;
esac
}
TARGET_ARCH="${TARGET_ARCH:-}"
IMAGE_TAG="${IMAGE_TAG:-}"
DOCKERFILE="${DOCKERFILE:-$REPO_ROOT/docker/ours-rp-metrics.Dockerfile}"
args=()
image_explicit=0
dockerfile_explicit=0
while [[ $# -gt 0 ]]; do
case "$1" in
--arch)
TARGET_ARCH="$(normalize_arch "$2")" || {
echo "unsupported target architecture: $2" >&2
exit 2
}
args+=("$1" "$TARGET_ARCH")
shift 2
;;
--image)
IMAGE_TAG="$2"
image_explicit=1
args+=("$1" "$2")
shift 2
;;
--dockerfile)
DOCKERFILE="$2"
dockerfile_explicit=1
args+=("$1" "$2")
shift 2
;;
*)
args+=("$1")
shift
;;
esac
done
if [[ -z "$TARGET_ARCH" ]]; then
cat >&2 <<'EOF'
missing target architecture.
Usage:
scripts/docker/build_docker_metrics_image.sh --arch amd64|arm64 [options]
EOF
exit 2
fi
if [[ -z "$IMAGE_TAG" ]]; then
SOURCE_COMMIT_SHORT="$(git -C "$REPO_ROOT" rev-parse --short=8 HEAD 2>/dev/null || echo unknown)"
if [[ -n "$(git -C "$REPO_ROOT" status --short 2>/dev/null)" ]]; then
SOURCE_COMMIT_SHORT="${SOURCE_COMMIT_SHORT}-dirty"
fi
IMAGE_TAG="ours-rp-metrics-${TARGET_ARCH}:${SOURCE_COMMIT_SHORT}"
fi
if [[ "$dockerfile_explicit" != "1" ]]; then
args=(--dockerfile "$DOCKERFILE" "${args[@]}")
fi
if [[ "$image_explicit" != "1" ]]; then
args=(--image "$IMAGE_TAG" "${args[@]}")
fi
exec "$SCRIPT_DIR/build_docker_runtime_image.sh" "${args[@]}"

View File

@@ -0,0 +1,295 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
TARGET_ARCH="${TARGET_ARCH:-}"
IMAGE_TAG="${IMAGE_TAG:-}"
BUILDER_IMAGE="${BUILDER_IMAGE:-rust:1-bookworm}"
RUNTIME_IMAGE="${RUNTIME_IMAGE:-debian:bookworm-slim}"
OUT_DIR="${OUT_DIR:-}"
DOCKERFILE="${DOCKERFILE:-$REPO_ROOT/docker/ours-rp-runtime.Dockerfile}"
BUILDER_NAME="${BUILDER_NAME:-default}"
INSTALL_BINFMT="${INSTALL_BINFMT:-1}"
SAVE_IMAGE="${SAVE_IMAGE:-1}"
LOAD_IMAGE="${LOAD_IMAGE:-1}"
usage() {
cat <<'USAGE'
Usage:
scripts/docker/build_docker_runtime_image.sh --arch amd64|arm64 [options]
Options:
--arch <arch> Target architecture: amd64|arm64
--image <tag> Docker image tag (default: ours-rp-runtime-<arch>:<git8>)
--out-dir <path> Directory for docker save tar.gz (default: target/<arch>-docker)
--dockerfile <path> Dockerfile path
--builder <name> buildx builder name
--builder-image <tag>
Builder base image (default: rust:1-bookworm)
--runtime-image <tag>
Runtime base image (default: debian:bookworm-slim)
--no-binfmt Do not install binfmt/qemu for cross-architecture builds
--no-save Build image but do not docker save it
--no-load Use buildx output tar instead of --load
-h, --help Show this help
USAGE
}
normalize_arch() {
case "$1" in
amd64|x86_64)
printf 'amd64\n'
;;
arm64|aarch64)
printf 'arm64\n'
;;
*)
return 1
;;
esac
}
safe_tag_name() {
printf '%s' "$1" | tr '/:' '--'
}
local_base_image_tag() {
local role="$1"
local arch="$2"
local source_image="$3"
printf 'ours-rp-base-%s-%s:%s\n' "$role" "$arch" "$(safe_tag_name "$source_image")"
}
platform_for_arch() {
printf 'linux/%s\n' "$1"
}
default_image_tag() {
local arch="$1"
local commit_short="$2"
printf 'ours-rp-runtime-%s:%s\n' "$arch" "$commit_short"
}
require_command() {
command -v "$1" >/dev/null 2>&1 || {
echo "missing required command: $1" >&2
exit 2
}
}
host_arch() {
local raw_arch
raw_arch="$(uname -m)"
normalize_arch "$raw_arch" || {
echo "unsupported host architecture: $raw_arch" >&2
exit 2
}
}
while [[ $# -gt 0 ]]; do
case "$1" in
--arch)
TARGET_ARCH="$(normalize_arch "$2")" || {
echo "unsupported target architecture: $2" >&2
exit 2
}
shift 2
;;
--image)
IMAGE_TAG="$2"
shift 2
;;
--out-dir)
OUT_DIR="$2"
shift 2
;;
--dockerfile)
DOCKERFILE="$2"
shift 2
;;
--builder)
BUILDER_NAME="$2"
shift 2
;;
--builder-image)
BUILDER_IMAGE="$2"
shift 2
;;
--runtime-image)
RUNTIME_IMAGE="$2"
shift 2
;;
--no-binfmt)
INSTALL_BINFMT=0
shift
;;
--no-save)
SAVE_IMAGE=0
shift
;;
--no-load)
LOAD_IMAGE=0
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
[[ -n "$TARGET_ARCH" ]] || {
echo "--arch is required" >&2
usage >&2
exit 2
}
SOURCE_COMMIT_FULL="$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || echo unknown)"
SOURCE_COMMIT_SHORT="$(git -C "$REPO_ROOT" rev-parse --short=8 HEAD 2>/dev/null || echo unknown)"
SOURCE_DIRTY="false"
if [[ -n "$(git -C "$REPO_ROOT" status --short 2>/dev/null)" ]]; then
SOURCE_DIRTY="true"
fi
BUILD_TIMESTAMP_UTC="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
if [[ -z "$IMAGE_TAG" ]]; then
default_revision="$SOURCE_COMMIT_SHORT"
if [[ "$SOURCE_DIRTY" == "true" ]]; then
default_revision="${default_revision}-dirty"
fi
IMAGE_TAG="$(default_image_tag "$TARGET_ARCH" "$default_revision")"
fi
if [[ -z "$OUT_DIR" ]]; then
OUT_DIR="$REPO_ROOT/target/${TARGET_ARCH}-docker"
fi
require_command docker
mkdir -p "$OUT_DIR"
target_platform="$(platform_for_arch "$TARGET_ARCH")"
local_host_arch="$(host_arch)"
builder_platform="$(platform_for_arch "$local_host_arch")"
builder_source_image="$BUILDER_IMAGE"
runtime_source_image="$RUNTIME_IMAGE"
builder_local_image="$(local_base_image_tag builder "$local_host_arch" "$builder_source_image")"
runtime_local_image="$(local_base_image_tag runtime "$TARGET_ARCH" "$runtime_source_image")"
if [[ "$INSTALL_BINFMT" == "1" && "$local_host_arch" != "$TARGET_ARCH" ]]; then
echo "installing binfmt/qemu for $TARGET_ARCH"
docker run --rm --privileged tonistiigi/binfmt --install "$TARGET_ARCH"
fi
# Pulling through the Docker daemon uses its configured proxy. Buildx then consumes
# these architecture-pinned local aliases without issuing separate registry metadata
# requests for the Dockerfile FROM instructions.
echo "pulling builder base image: $builder_source_image ($builder_platform)"
docker pull --platform "$builder_platform" "$builder_source_image"
docker tag "$builder_source_image" "$builder_local_image"
echo "pulling runtime base image: $runtime_source_image ($target_platform)"
docker pull --platform "$target_platform" "$runtime_source_image"
docker tag "$runtime_source_image" "$runtime_local_image"
BUILDER_IMAGE="$builder_local_image"
RUNTIME_IMAGE="$runtime_local_image"
if [[ "$BUILDER_NAME" != "default" ]] && ! docker buildx inspect "$BUILDER_NAME" >/dev/null 2>&1; then
docker buildx create --name "$BUILDER_NAME" --driver docker-container --use >/dev/null
else
docker buildx use "$BUILDER_NAME" >/dev/null
fi
docker buildx inspect --bootstrap >/dev/null
metadata_path="$OUT_DIR/$(safe_tag_name "$IMAGE_TAG").build-metadata.json"
tar_path="$OUT_DIR/$(safe_tag_name "$IMAGE_TAG").tar.gz"
build_log="$OUT_DIR/$(safe_tag_name "$IMAGE_TAG").build.log"
echo "building $target_platform image: $IMAGE_TAG"
echo "repo: $REPO_ROOT"
echo "dockerfile: $DOCKERFILE"
echo "builder_source_image: $builder_source_image"
echo "runtime_source_image: $runtime_source_image"
echo "builder_local_image: $BUILDER_IMAGE"
echo "runtime_local_image: $RUNTIME_IMAGE"
echo "source_commit: $SOURCE_COMMIT_FULL"
echo "source_dirty: $SOURCE_DIRTY"
echo "build_timestamp_utc: $BUILD_TIMESTAMP_UTC"
start_epoch="$(date +%s)"
if [[ "$LOAD_IMAGE" == "1" ]]; then
docker buildx build \
--platform "$target_platform" \
--builder "$BUILDER_NAME" \
--pull=false \
--load \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--build-arg "BUILDER_IMAGE=$BUILDER_IMAGE" \
--build-arg "RUNTIME_IMAGE=$RUNTIME_IMAGE" \
--build-arg "SOURCE_COMMIT=$SOURCE_COMMIT_FULL" \
--build-arg "SOURCE_DIRTY=$SOURCE_DIRTY" \
--build-arg "BUILD_TIMESTAMP_UTC=$BUILD_TIMESTAMP_UTC" \
--metadata-file "$metadata_path" \
-t "$IMAGE_TAG" \
-f "$DOCKERFILE" \
"$REPO_ROOT" 2>&1 | tee "$build_log"
else
raw_tar_path="$OUT_DIR/$(safe_tag_name "$IMAGE_TAG").tar"
docker buildx build \
--platform "$target_platform" \
--builder "$BUILDER_NAME" \
--pull=false \
--output "type=docker,dest=$raw_tar_path" \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--build-arg "BUILDER_IMAGE=$BUILDER_IMAGE" \
--build-arg "RUNTIME_IMAGE=$RUNTIME_IMAGE" \
--build-arg "SOURCE_COMMIT=$SOURCE_COMMIT_FULL" \
--build-arg "SOURCE_DIRTY=$SOURCE_DIRTY" \
--build-arg "BUILD_TIMESTAMP_UTC=$BUILD_TIMESTAMP_UTC" \
--metadata-file "$metadata_path" \
-t "$IMAGE_TAG" \
-f "$DOCKERFILE" \
"$REPO_ROOT" 2>&1 | tee "$build_log"
gzip -f "$raw_tar_path"
tar_path="${raw_tar_path}.gz"
fi
elapsed_secs=$(( $(date +%s) - start_epoch ))
if [[ "$SAVE_IMAGE" == "1" && "$LOAD_IMAGE" == "1" ]]; then
echo "saving image to $tar_path"
docker save "$IMAGE_TAG" | gzip -c > "$tar_path"
fi
tar_sha256=""
if [[ -f "$tar_path" ]]; then
tar_sha256="$(sha256sum "$tar_path" | awk '{print $1}')"
fi
{
echo "image=$IMAGE_TAG"
echo "platform=$target_platform"
echo "arch=$TARGET_ARCH"
echo "builder_image=$builder_source_image"
echo "runtime_image=$runtime_source_image"
echo "builder_local_image=$BUILDER_IMAGE"
echo "runtime_local_image=$RUNTIME_IMAGE"
echo "elapsed_secs=$elapsed_secs"
echo "metadata=$metadata_path"
echo "tar=$tar_path"
echo "tar_size_bytes=$(wc -c < "$tar_path" 2>/dev/null || echo 0)"
echo "tar_sha256=$tar_sha256"
echo "source_commit=$SOURCE_COMMIT_FULL"
echo "source_commit_short=$SOURCE_COMMIT_SHORT"
echo "source_dirty=$SOURCE_DIRTY"
echo "build_timestamp_utc=$BUILD_TIMESTAMP_UTC"
echo "git_commit=$SOURCE_COMMIT_FULL"
echo "git_status_count=$(git -C "$REPO_ROOT" status --short 2>/dev/null | wc -l | tr -d ' ')"
echo "built_at_utc=$BUILD_TIMESTAMP_UTC"
} > "$OUT_DIR/$(safe_tag_name "$IMAGE_TAG").build-summary.env"
echo "build complete: elapsed=${elapsed_secs}s tar=$tar_path"

View File

@@ -0,0 +1,41 @@
{
"schemaVersion": 1,
"defaultRirs": ["afrinic", "apnic", "arin", "lacnic", "ripe"],
"experiments": [
{
"id": "sync-ours-rsync-only",
"left": { "rpKind": "ours", "mode": "standard", "protocol": "rrdp+rsync", "rsyncScope": "module-root" },
"right": { "rpKind": "ours", "mode": "standard", "protocol": "rsync-only", "rsyncScope": "module-root" }
},
{
"id": "sync-rpki-client-rsync-only",
"left": { "rpKind": "rpki-client", "mode": "standard", "protocol": "rrdp+rsync" },
"right": { "rpKind": "rpki-client", "mode": "standard", "protocol": "rsync-only" }
},
{
"id": "strict-name",
"left": { "rpKind": "ours", "mode": "standard", "protocol": "rrdp+rsync" },
"right": { "rpKind": "ours", "mode": "strict-name", "protocol": "rrdp+rsync" }
},
{
"id": "strict-cms-der",
"left": { "rpKind": "ours", "mode": "standard", "protocol": "rrdp+rsync" },
"right": { "rpKind": "ours", "mode": "strict-cms-der", "protocol": "rrdp+rsync" }
},
{
"id": "strict-signed-attrs",
"left": { "rpKind": "ours", "mode": "standard", "protocol": "rrdp+rsync" },
"right": { "rpKind": "ours", "mode": "strict-signed-attrs", "protocol": "rrdp+rsync" }
},
{
"id": "strict-all",
"left": { "rpKind": "ours", "mode": "standard", "protocol": "rrdp+rsync" },
"right": { "rpKind": "ours", "mode": "strict-all", "protocol": "rrdp+rsync" }
},
{
"id": "rp-implementation-standard",
"left": { "rpKind": "ours", "mode": "standard", "protocol": "rrdp+rsync" },
"right": { "rpKind": "rpki-client", "mode": "standard", "protocol": "rrdp+rsync" }
}
]
}

View File

@@ -0,0 +1,431 @@
#!/usr/bin/env python3
import argparse
import hashlib
import json
import os
import platform
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
RIR_FIXTURES = {
"afrinic": {
"tal": "tal/afrinic.tal",
"ta": "ta/afrinic-ta.cer",
},
"apnic": {
"tal": "tal/apnic-rfc7730-https.tal",
"ta": "ta/apnic-ta.cer",
},
"arin": {
"tal": "tal/arin.tal",
"ta": "ta/arin-ta.cer",
},
"lacnic": {
"tal": "tal/lacnic.tal",
"ta": "ta/lacnic-ta.cer",
},
"ripe": {
"tal": "tal/ripe-ncc.tal",
"ta": "ta/ripe-ncc-ta.cer",
},
}
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def sha256_file(path: Path) -> str:
hasher = hashlib.sha256()
with path.open("rb") as file:
for chunk in iter(lambda: file.read(1024 * 1024), b""):
hasher.update(chunk)
return hasher.hexdigest()
def read_tal_uris(path: Path) -> list[str]:
uris: list[str] = []
with path.open("r", encoding="utf-8") as file:
for line in file:
item = line.strip()
if not item or item.startswith("#"):
if uris:
break
continue
if item.startswith(("rsync://", "https://", "http://")):
uris.append(item)
continue
if uris:
break
return uris
def first_uri(uris: list[str], prefixes: tuple[str, ...]) -> str | None:
for uri in uris:
if uri.startswith(prefixes):
return uri
return None
def parse_rirs(raw: str) -> list[str]:
rirs = [item.strip().lower() for item in raw.split(",") if item.strip()]
if not rirs:
raise SystemExit("RIR list must not be empty")
invalid = [item for item in rirs if item not in RIR_FIXTURES]
if invalid:
raise SystemExit(
f"invalid RIR(s): {','.join(invalid)}; allowed: {','.join(RIR_FIXTURES)}"
)
return rirs
def rel_or_abs(path: Path, root: Path | None) -> str:
path = path.resolve()
if root is not None:
try:
return path.relative_to(root.resolve()).as_posix()
except ValueError:
pass
return path.as_posix()
def git_commit(repo_root: Path) -> str | None:
try:
return subprocess.check_output(
["git", "-C", str(repo_root), "rev-parse", "--short", "HEAD"],
text=True,
stderr=subprocess.DEVNULL,
).strip()
except (subprocess.CalledProcessError, FileNotFoundError):
return None
def write_json(path: Path, value: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
def build_fixture_proof(
fixture_dir: Path,
rirs: list[str],
repo_root: Path | None,
ta_online_fetch_observed: bool,
) -> dict[str, Any]:
trust_anchors = []
for rir in rirs:
mapping = RIR_FIXTURES[rir]
tal_path = fixture_dir / mapping["tal"]
ta_path = fixture_dir / mapping["ta"]
if not tal_path.is_file():
raise SystemExit(f"missing TAL fixture for {rir}: {tal_path}")
if not ta_path.is_file():
raise SystemExit(f"missing TA fixture for {rir}: {ta_path}")
tal_uris = read_tal_uris(tal_path)
trust_anchors.append(
{
"rir": rir,
"talPath": rel_or_abs(tal_path, repo_root),
"taPath": rel_or_abs(ta_path, repo_root),
"talUri": first_uri(tal_uris, ("https://", "http://")),
"taRsyncUri": first_uri(tal_uris, ("rsync://",)),
"talSha256": sha256_file(tal_path),
"taCertificateSha256": sha256_file(ta_path),
"talBytes": tal_path.stat().st_size,
"taCertificateBytes": ta_path.stat().st_size,
"taFixturePinned": not ta_online_fetch_observed,
"taOnlineFetchObserved": ta_online_fetch_observed,
}
)
return {
"schemaVersion": 1,
"generatedBy": "feature035-experiment-driver",
"generatedAtUtc": utc_now(),
"fixtureDir": rel_or_abs(fixture_dir, repo_root),
"all5": set(rirs) == set(RIR_FIXTURES),
"rirs": rirs,
"trustAnchors": trust_anchors,
}
def parse_csv(raw: str) -> list[str]:
if not raw:
return []
return [item.strip() for item in raw.split(",") if item.strip()]
def optional_path(raw: str | None, repo_root: Path | None) -> str | None:
if raw is None:
return None
return rel_or_abs(Path(raw), repo_root)
def build_run_meta(args: argparse.Namespace) -> dict[str, Any]:
rirs = parse_rirs(args.rirs)
repo_root = Path(args.repo_root).resolve() if args.repo_root else None
argv = json.loads(args.argv_json) if args.argv_json else []
env_whitelist = json.loads(args.env_json) if args.env_json else {}
fixture_proof_summary = (
json.loads(args.fixture_proof_summary_json)
if args.fixture_proof_summary_json
else None
)
fixture_proof = None
if args.fixture_proof:
fixture_proof_path = Path(args.fixture_proof)
if fixture_proof_path.is_file():
fixture_proof = json.loads(fixture_proof_path.read_text(encoding="utf-8"))
if not isinstance(argv, list):
raise SystemExit("--argv-json must decode to a JSON array")
if not isinstance(env_whitelist, dict):
raise SystemExit("--env-json must decode to a JSON object")
if fixture_proof_summary is not None and not isinstance(fixture_proof_summary, dict):
raise SystemExit("--fixture-proof-summary-json must decode to a JSON object")
return {
"schemaVersion": 1,
"generatedBy": "feature035-experiment-driver",
"generatedAtUtc": utc_now(),
"experimentId": args.experiment_id,
"side": args.side,
"sideLabel": args.side_label,
"step": args.step,
"runId": args.run_id,
"liveRun": not args.replay_used,
"replayUsed": args.replay_used,
"rp": {
"kind": args.rp_kind,
"binary": args.rp_binary,
"version": args.rp_version,
"gitCommit": args.rp_git_commit,
"mode": args.rp_mode,
"protocolMode": args.protocol_mode,
"strictPolicies": parse_csv(args.strict_policies),
},
"scope": {
"rirs": rirs,
"all5": set(rirs) == set(RIR_FIXTURES),
},
"command": {
"argv": argv,
"cwd": args.cwd,
"envWhitelist": env_whitelist,
},
"state": {
"resetBeforeRun": args.reset_before_run,
"stateRoot": args.state_root,
"db": args.db,
"repoBytesDb": args.repo_bytes_db,
"rawStoreDb": args.raw_store_db,
"rsyncMirrorRoot": args.rsync_mirror_root,
"cacheRoot": args.cache_root,
},
"artifacts": {
"ccr": optional_path(args.ccr, repo_root),
"cir": optional_path(args.cir, repo_root),
"runMeta": optional_path(str(args.out), repo_root),
"fixtureProof": optional_path(args.fixture_proof, repo_root),
"reportJson": optional_path(args.report_json, repo_root),
"stageTimingJson": optional_path(args.stage_timing_json, repo_root),
"stdoutLog": optional_path(args.stdout_log, repo_root),
"stderrLog": optional_path(args.stderr_log, repo_root),
"processTime": optional_path(args.process_time, repo_root),
"vrpsCsv": optional_path(args.vrps_csv, repo_root),
"vapsCsv": optional_path(args.vaps_csv, repo_root),
},
"fixtureProof": fixture_proof,
"fixtureProofSummary": fixture_proof_summary,
"metrics": {
"exitCode": args.exit_code,
"wallMs": args.wall_ms,
"maxRssKb": args.max_rss_kb,
"vrps": args.vrps,
"vaps": args.vaps,
"publicationPoints": args.publication_points,
"warnings": args.warnings,
"cirObjectCount": args.cir_object_count,
"cirRejectCount": args.cir_reject_count,
"cirTrustAnchorCount": args.cir_trust_anchor_count,
"ccrStateDigest": args.ccr_state_digest,
},
"environment": {
"host": args.host or platform.node(),
"platform": args.platform or platform.platform(),
},
}
def command_fixture_proof(args: argparse.Namespace) -> None:
repo_root = Path(args.repo_root).resolve() if args.repo_root else None
proof = build_fixture_proof(
fixture_dir=Path(args.fixture_dir),
rirs=parse_rirs(args.rirs),
repo_root=repo_root,
ta_online_fetch_observed=args.ta_online_fetch_observed,
)
write_json(Path(args.out), proof)
def command_run_meta(args: argparse.Namespace) -> None:
write_json(Path(args.out), build_run_meta(args))
def command_dry_run_bundle(args: argparse.Namespace) -> None:
out_dir = Path(args.out_dir)
repo_root = Path(args.repo_root).resolve() if args.repo_root else Path.cwd().resolve()
fixture_proof = out_dir / "fixture-proof.json"
command_fixture_proof(
argparse.Namespace(
fixture_dir=args.fixture_dir,
rirs=args.rirs,
repo_root=str(repo_root),
out=str(fixture_proof),
ta_online_fetch_observed=False,
)
)
for side, side_label in (("left", "A"), ("right", "B")):
run_dir = out_dir / side_label / "snapshot"
meta_args = argparse.Namespace(
out=run_dir / "run-meta.json",
repo_root=str(repo_root),
experiment_id=args.experiment_id,
side=side,
side_label=side_label,
step="snapshot",
run_id=f"{side_label}-snapshot-dry-run",
replay_used=False,
rp_kind="ours" if side_label == "A" else "rpki-client",
rp_binary=f"bin/{'rpki' if side_label == 'A' else 'rpki-client'}",
rp_version="dry-run",
rp_git_commit=git_commit(repo_root),
rp_mode="standard",
protocol_mode="rrdp+rsync",
strict_policies="",
rirs=args.rirs,
argv_json=json.dumps(["dry-run"]),
env_json=json.dumps({"RPKI_PROGRESS_LOG": "1"}),
cwd=str(out_dir),
reset_before_run=True,
state_root=str(out_dir / side_label / "state"),
db=str(out_dir / side_label / "state" / "work-db"),
repo_bytes_db=str(out_dir / side_label / "state" / "repo-bytes.db"),
raw_store_db=str(out_dir / side_label / "state" / "raw-store.db"),
rsync_mirror_root=str(out_dir / side_label / "state" / "rsync-mirror"),
cache_root=str(out_dir / side_label / "state" / "cache"),
ccr=str(run_dir / "result.ccr"),
cir=str(run_dir / "result.cir"),
fixture_proof=str(fixture_proof),
report_json=str(run_dir / "report.json"),
stage_timing_json=str(run_dir / "stage-timing.json"),
stdout_log=str(run_dir / "stdout.log"),
stderr_log=str(run_dir / "stderr.log"),
process_time=str(run_dir / "process-time.txt"),
vrps_csv=str(run_dir / "vrps.csv"),
vaps_csv=str(run_dir / "vaps.csv"),
exit_code=0,
wall_ms=0,
max_rss_kb=0,
vrps=0,
vaps=0,
publication_points=0,
warnings=0,
cir_object_count=0,
cir_reject_count=0,
cir_trust_anchor_count=len(parse_rirs(args.rirs)),
ccr_state_digest=None,
fixture_proof_summary_json=json.dumps(
{
"taFixturePinned": True,
"taOnlineFetchObserved": False,
"trustAnchorCount": len(parse_rirs(args.rirs)),
}
),
)
command_run_meta(meta_args)
def add_run_meta_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--out", required=True)
parser.add_argument("--repo-root")
parser.add_argument("--experiment-id", required=True)
parser.add_argument("--side", choices=["left", "right"], required=True)
parser.add_argument("--side-label", choices=["A", "B"], required=True)
parser.add_argument("--step", choices=["snapshot", "delta"], required=True)
parser.add_argument("--run-id", required=True)
parser.add_argument("--replay-used", action="store_true")
parser.add_argument("--rp-kind", required=True)
parser.add_argument("--rp-binary", required=True)
parser.add_argument("--rp-version")
parser.add_argument("--rp-git-commit")
parser.add_argument("--rp-mode", default="standard")
parser.add_argument("--protocol-mode", default="rrdp+rsync")
parser.add_argument("--strict-policies", default="")
parser.add_argument("--rirs", default="afrinic,apnic,arin,lacnic,ripe")
parser.add_argument("--argv-json")
parser.add_argument("--env-json")
parser.add_argument("--cwd", default=os.getcwd())
parser.add_argument("--reset-before-run", action="store_true")
parser.add_argument("--state-root")
parser.add_argument("--db")
parser.add_argument("--repo-bytes-db")
parser.add_argument("--raw-store-db")
parser.add_argument("--rsync-mirror-root")
parser.add_argument("--cache-root")
parser.add_argument("--ccr")
parser.add_argument("--cir")
parser.add_argument("--fixture-proof")
parser.add_argument("--fixture-proof-summary-json")
parser.add_argument("--report-json")
parser.add_argument("--stage-timing-json")
parser.add_argument("--stdout-log")
parser.add_argument("--stderr-log")
parser.add_argument("--process-time")
parser.add_argument("--vrps-csv")
parser.add_argument("--vaps-csv")
parser.add_argument("--exit-code", type=int)
parser.add_argument("--wall-ms", type=int)
parser.add_argument("--max-rss-kb", type=int)
parser.add_argument("--vrps", type=int)
parser.add_argument("--vaps", type=int)
parser.add_argument("--publication-points", type=int)
parser.add_argument("--warnings", type=int)
parser.add_argument("--cir-object-count", type=int)
parser.add_argument("--cir-reject-count", type=int)
parser.add_argument("--cir-trust-anchor-count", type=int)
parser.add_argument("--ccr-state-digest")
parser.add_argument("--host")
parser.add_argument("--platform")
def main() -> None:
parser = argparse.ArgumentParser(description="Feature #035 CCR/CIR experiment bundle helpers")
subparsers = parser.add_subparsers(dest="command", required=True)
fixture = subparsers.add_parser("fixture-proof")
fixture.add_argument("--fixture-dir", default="tests/fixtures")
fixture.add_argument("--rirs", default="afrinic,apnic,arin,lacnic,ripe")
fixture.add_argument("--repo-root")
fixture.add_argument("--out", required=True)
fixture.add_argument("--ta-online-fetch-observed", action="store_true")
fixture.set_defaults(func=command_fixture_proof)
run_meta = subparsers.add_parser("run-meta")
add_run_meta_args(run_meta)
run_meta.set_defaults(func=command_run_meta)
dry_run = subparsers.add_parser("dry-run-bundle")
dry_run.add_argument("--out-dir", required=True)
dry_run.add_argument("--repo-root", default=".")
dry_run.add_argument("--fixture-dir", default="tests/fixtures")
dry_run.add_argument("--rirs", default="afrinic,apnic,arin,lacnic,ripe")
dry_run.add_argument("--experiment-id", default="m2-dry-run")
dry_run.set_defaults(func=command_dry_run_bundle)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,10 @@
{
"schemaVersion": 1,
"rirs": {
"afrinic": { "tal": "tal/afrinic.tal", "ta": "ta/afrinic-ta.cer" },
"apnic": { "tal": "tal/apnic-rfc7730-https.tal", "ta": "ta/apnic-ta.cer" },
"arin": { "tal": "tal/arin.tal", "ta": "ta/arin-ta.cer" },
"lacnic": { "tal": "tal/lacnic.tal", "ta": "ta/lacnic-ta.cer" },
"ripe": { "tal": "tal/ripe-ncc.tal", "ta": "ta/ripe-ncc-ta.cer" }
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,939 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
from concurrent.futures import ThreadPoolExecutor, as_completed
import hashlib
import json
import os
import shlex
import subprocess
import sys
import time
from pathlib import Path
from typing import Any
SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parents[2]
DEV_ROOT = REPO_ROOT.parents[1]
FEATURE035_DIR = REPO_ROOT / "scripts" / "experiments" / "feature035"
FIXTURE_MANIFEST_PATH = FEATURE035_DIR / "fixture-manifest.json"
PORTABLE_ROOT = DEV_ROOT / "rpki-client-portable"
CACHED_CIR_RPKI_CLIENT = DEV_ROOT / ".cache" / "rpki-client-9.8-cir" / "rpki-client"
CACHED_CIR_LIBTLS = DEV_ROOT / ".cache" / "rpki-client-9.8-cir" / "libtls.so.28"
DEFAULT_RIRS = ["afrinic", "apnic", "arin", "lacnic", "ripe"]
def run_local(argv: list[str], *, cwd: Path | None = None, capture: bool = False, check: bool = True) -> subprocess.CompletedProcess[str]:
result = subprocess.run(argv, cwd=str(cwd) if cwd else None, text=True, capture_output=capture, check=False)
if check and result.returncode != 0:
raise SystemExit(
f"command failed ({result.returncode}): {' '.join(shlex.quote(x) for x in argv)}\n"
f"stdout:\n{result.stdout or ''}\nstderr:\n{result.stderr or ''}"
)
return result
def ssh_script(target: str, script: str, *, capture: bool = False, check: bool = True) -> subprocess.CompletedProcess[str]:
result = subprocess.run(["ssh", target, "bash", "-s"], input=script, text=True, capture_output=capture, check=False)
if check and result.returncode != 0:
raise SystemExit(f"remote script failed ({result.returncode}) on {target}\n{result.stdout}\n{result.stderr}")
return result
def rsync_to_remote(target: str, source: Path, destination: str | Path) -> None:
run_local(["rsync", "-a", str(source), f"{target}:{destination}"])
def rsync_dir_to_remote(target: str, source: Path, destination: str | Path) -> None:
run_local(["rsync", "-a", f"{source}/", f"{target}:{destination}/"])
def rsync_from_remote(target: str, source: str | Path, destination: Path) -> None:
destination.mkdir(parents=True, exist_ok=True)
run_local(["rsync", "-a", f"{target}:{source}/", f"{destination}/"])
def rsync_run_artifacts_from_remote(target: str, source: str | Path, destination: Path) -> None:
destination.mkdir(parents=True, exist_ok=True)
rsync_base = ["rsync", "-az", "--ignore-missing-args", "--partial", "--partial-dir=.rsync-partial"]
for name in [
"result.ccr",
"result.cir",
"report.json",
"vrps.csv",
"vaps.csv",
"process-time.txt",
"remote-run-meta.json",
"exit-code.txt",
"started-at.txt",
"finished-at.txt",
"stdout.log",
"stderr.log",
]:
run_local([*rsync_base, f"{target}:{source}/{name}", f"{destination}/"])
def rsync_remote_analysis_from_remote(target: str, remote_exp_root: str | Path, local_exp_root: Path) -> None:
local_exp_root.mkdir(parents=True, exist_ok=True)
rsync_base = ["rsync", "-az", "--ignore-missing-args", "--partial", "--partial-dir=.rsync-partial"]
for name in [
"left-sequence.jsonl",
"right-sequence.jsonl",
"run-progress.json",
"sequence-triage-time.txt",
"sequence-triage/sequence-triage.json",
]:
destination = local_exp_root / Path(name).parent
destination.mkdir(parents=True, exist_ok=True)
run_local([*rsync_base, f"{target}:{remote_exp_root}/{name}", f"{destination}/"])
def same_remote_location(
left_target: str,
left_root: str | Path,
right_target: str,
right_root: str | Path,
) -> bool:
return left_target == right_target and str(left_root) == str(right_root)
def load_json(path: Path) -> Any:
with path.open("r", encoding="utf-8") as handle:
return json.load(handle)
def write_json(path: Path, value: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as handle:
json.dump(value, handle, indent=2, sort_keys=True, ensure_ascii=False)
handle.write("\n")
def append_jsonl(path: Path, value: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(value, sort_keys=True, ensure_ascii=False))
handle.write("\n")
def utc_stamp() -> str:
return time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())
def rfc3339_now() -> str:
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
def fixture_manifest() -> dict[str, Any]:
return load_json(FIXTURE_MANIFEST_PATH)
def fixture_name(rir: str, kind: str) -> str:
return Path(fixture_manifest()["rirs"][rir][kind]).name
def fixture_desc(rir: str) -> str:
return {
"afrinic": "afrinic",
"apnic": "apnic-rfc7730-https",
"arin": "arin",
"lacnic": "lacnic",
"ripe": "ripe-ncc",
}[rir]
def cir_tal_uri_for_rir(rir: str) -> str:
return {
"afrinic": "https://rpki.afrinic.net/tal/afrinic.tal",
"apnic": "https://rpki.apnic.net/tal/apnic-rfc7730-https.tal",
"arin": "https://www.arin.net/resources/manage/rpki/arin.tal",
"lacnic": "https://www.lacnic.net/innovaportal/file/4983/1/lacnic.tal",
"ripe": "https://tal.rpki.ripe.net/ripe-ncc.tal",
}[rir]
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def parse_elapsed_to_ms(raw: str) -> int:
raw = raw.strip()
if not raw:
return 0
if "-" in raw:
days, raw = raw.split("-", 1)
else:
days = "0"
parts = raw.split(":")
if len(parts) == 3:
hours, minutes, seconds = parts
elif len(parts) == 2:
hours = "0"
minutes, seconds = parts
else:
hours = "0"
minutes = "0"
seconds = parts[0]
return int(round((int(days) * 86400 + int(hours) * 3600 + int(minutes) * 60 + float(seconds)) * 1000))
def parse_time_file(path: Path) -> dict[str, Any]:
data: dict[str, Any] = {}
if not path.is_file():
return data
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
if "Elapsed (wall clock) time" in line:
elapsed = line.rsplit(":", 1)[1] if "):" not in line else line.rsplit("):", 1)[1]
data["wallMs"] = parse_elapsed_to_ms(elapsed)
elif "Maximum resident set size" in line:
try:
data["maxRssKb"] = int(line.rsplit(":", 1)[1].strip())
except ValueError:
pass
return data
def rpki_client_bin_path() -> Path:
primary = PORTABLE_ROOT / "src" / "rpki-client"
for candidate in (primary, CACHED_CIR_RPKI_CLIENT):
if not candidate.is_file():
continue
smoke = run_local([str(candidate), "-T", "invalid"], capture=True, check=False)
if "--ta-fixture requires <tal>:<path>" in (smoke.stderr + smoke.stdout):
return candidate
raise SystemExit("rpki-client binary lacks CIR/TA fixture support; checkout feature/cir-output-for-rp-compare or restore .cache/rpki-client-9.8-cir/rpki-client")
def detect_libtls_path(rpki_client_bin: Path) -> Path:
if CACHED_CIR_LIBTLS.is_file():
return CACHED_CIR_LIBTLS
ldd = run_local(["ldd", str(rpki_client_bin)], capture=True)
for line in ldd.stdout.splitlines():
if "libtls.so.28" not in line or "=>" not in line:
continue
candidate = Path(line.split("=>", 1)[1].strip().split(" ", 1)[0])
if candidate.is_file():
return candidate
fallback = DEV_ROOT / ".cache" / "rpki-client-9.8-cir" / "libtls.so.28"
if fallback.is_file():
return fallback
raise SystemExit("unable to locate libtls.so.28 for rpki-client")
def build_tool_binaries() -> None:
run_local([
"cargo", "build", "--release",
"--bin", "rpki",
"--bin", "sequence_triage_ccr_cir",
"--bin", "cir_dump_reject_list",
], cwd=REPO_ROOT)
_ = rpki_client_bin_path()
def validate_remote_disk(ssh_target: str) -> None:
script = r'''
set -euo pipefail
df -h /data / || true
python3 - <<'PY'
import shutil
for path in ['/data', '/']:
try:
usage = shutil.disk_usage(path)
except FileNotFoundError:
continue
used = usage.used / usage.total if usage.total else 0
print(f'{path} used={used:.2%}')
if used >= 0.90:
raise SystemExit(f'{path} disk usage >= 90%; cleanup required before all5 sequence experiment')
PY
'''
ssh_script(ssh_target, script)
def prepare_remote(ssh_target: str, remote_root: Path, needs_rpki_client: bool) -> None:
validate_remote_disk(ssh_target)
preflight = (
"set -euo pipefail; "
"systemctl disable --now rpki-client.timer >/dev/null 2>&1 || true; "
"systemctl stop rpki-client.service >/dev/null 2>&1 || true; "
"pkill -x rpki-client >/dev/null 2>&1 || true; "
"pkill -x routinator >/dev/null 2>&1 || true; "
f"mkdir -p {shlex.quote(str(remote_root / 'bin'))} {shlex.quote(str(remote_root / 'lib'))} "
f"{shlex.quote(str(remote_root / 'fixtures' / 'tal'))} {shlex.quote(str(remote_root / 'fixtures' / 'ta'))} "
f"{shlex.quote(str(remote_root / 'experiments'))}; "
f"df -h /data / > {shlex.quote(str(remote_root / 'df-before.txt'))} 2>&1 || true; "
f"free -h > {shlex.quote(str(remote_root / 'free-before.txt'))} 2>&1 || true"
)
ssh_script(ssh_target, preflight)
rsync_dir_to_remote(ssh_target, REPO_ROOT / "tests" / "fixtures" / "tal", remote_root / "fixtures" / "tal")
rsync_dir_to_remote(ssh_target, REPO_ROOT / "tests" / "fixtures" / "ta", remote_root / "fixtures" / "ta")
rsync_to_remote(ssh_target, REPO_ROOT / "target" / "release" / "rpki", remote_root / "bin" / "rpki")
rsync_to_remote(ssh_target, REPO_ROOT / "target" / "release" / "sequence_triage_ccr_cir", remote_root / "bin" / "sequence_triage_ccr_cir")
rsync_to_remote(ssh_target, REPO_ROOT / "target" / "release" / "cir_dump_reject_list", remote_root / "bin" / "cir_dump_reject_list")
if needs_rpki_client:
rpki_client_bin = rpki_client_bin_path()
rsync_to_remote(ssh_target, rpki_client_bin, remote_root / "bin" / "rpki-client")
rsync_to_remote(ssh_target, detect_libtls_path(rpki_client_bin), remote_root / "lib" / "libtls.so.28")
def prepare_remote_once(
prepared: dict[tuple[str, str], bool],
ssh_target: str,
remote_root: Path,
needs_rpki_client: bool,
) -> None:
key = (ssh_target, str(remote_root))
already_has_rpki_client = prepared.get(key)
if already_has_rpki_client is not None and (already_has_rpki_client or not needs_rpki_client):
return
prepare_remote(ssh_target, remote_root, needs_rpki_client)
prepared[key] = bool(already_has_rpki_client or needs_rpki_client)
def side_config(name: str) -> dict[str, Any]:
if name == "ours-standard":
return {"rpKind": "ours", "mode": "standard", "protocol": "rrdp+rsync", "rsyncScope": "module-root"}
if name == "ours-strict-all":
return {
"rpKind": "ours",
"mode": "strict-all",
"protocol": "rrdp+rsync",
"rsyncScope": "module-root",
"strictPolicies": "name,cms-der,signed-attrs",
}
if name == "rpki-client-standard":
return {"rpKind": "rpki-client", "mode": "standard", "protocol": "rrdp+rsync"}
raise SystemExit(f"unknown side config: {name}")
def parse_rirs(raw: str) -> list[str]:
rirs = [item.strip() for item in raw.split(",") if item.strip()]
if not rirs:
raise SystemExit("--rirs must contain at least one RIR")
seen: set[str] = set()
invalid: list[str] = []
duplicate: list[str] = []
for rir in rirs:
if rir not in DEFAULT_RIRS:
invalid.append(rir)
if rir in seen:
duplicate.append(rir)
seen.add(rir)
if invalid:
raise SystemExit(f"unsupported RIR(s): {','.join(invalid)}; valid values: {','.join(DEFAULT_RIRS)}")
if duplicate:
raise SystemExit(f"duplicate RIR(s): {','.join(duplicate)}")
return rirs
def build_remote_command(remote_root: Path, side_name: str, side: dict[str, Any], side_label: str, seq: int, rirs: list[str]) -> tuple[Path, str]:
run_dir = remote_root / "experiments" / "sequence" / side_label / f"run_{seq:04d}"
state_dir = remote_root / "experiments" / "sequence" / side_label / "state" / side["rpKind"]
sync_mode = "snapshot" if seq == 1 else "delta"
ensure = [f"mkdir -p {shlex.quote(str(run_dir))}", f"chmod 0777 {shlex.quote(str(run_dir))}"]
if side["rpKind"] == "ours":
if seq == 1:
ensure.append(f"rm -rf {shlex.quote(str(state_dir))}")
ensure.extend([
f"mkdir -p {shlex.quote(str(state_dir / 'work-db'))} {shlex.quote(str(state_dir / 'rsync-mirror'))}",
f"chmod -R 0777 {shlex.quote(str(state_dir.parent))}",
])
argv = [
str(remote_root / "bin" / "rpki"),
"--db", str(state_dir / "work-db"),
"--raw-store-db", str(state_dir / "raw-store.db"),
"--repo-bytes-db", str(state_dir / "repo-bytes.db"),
"--rsync-scope", side.get("rsyncScope", "module-root"),
]
if side.get("strictPolicies"):
argv.extend(["--strict", str(side["strictPolicies"])])
for rir in rirs:
argv.extend(["--tal-path", str(remote_root / "fixtures" / "tal" / fixture_name(rir, "tal"))])
argv.extend(["--ta-path", str(remote_root / "fixtures" / "ta" / fixture_name(rir, "ta"))])
argv.extend(["--report-json", str(run_dir / "report.json"), "--report-json-compact"])
argv.extend(["--ccr-out", str(run_dir / "result.ccr"), "--cir-enable", "--cir-out", str(run_dir / "result.cir")])
for rir in rirs:
argv.extend(["--cir-tal-uri", cir_tal_uri_for_rir(rir)])
argv.extend(["--vrps-csv-out", str(run_dir / "vrps.csv"), "--vaps-csv-out", str(run_dir / "vaps.csv")])
prefix = "env RPKI_PROGRESS_LOG=1 RPKI_PROGRESS_SLOW_SECS=10 /usr/bin/time"
else:
if seq == 1:
ensure.append(f"rm -rf {shlex.quote(str(state_dir))}")
ensure.extend([
f"mkdir -p {shlex.quote(str(state_dir / 'cache' / 'fixtures'))}",
f"touch {shlex.quote(str(state_dir / 'rpki-client-skiplist'))}",
f"chmod -R 0777 {shlex.quote(str(state_dir.parent))}",
])
for rir in rirs:
ensure.append(
f"cp -f {shlex.quote(str(remote_root / 'fixtures' / 'ta' / fixture_name(rir, 'ta')))} "
f"{shlex.quote(str(state_dir / 'cache' / 'fixtures' / fixture_name(rir, 'ta')))}"
)
argv = [str(remote_root / "bin" / "rpki-client"), "-vv", "-S", str(state_dir / "rpki-client-skiplist")]
for rir in rirs:
argv.extend(["-t", str(remote_root / "fixtures" / "tal" / fixture_name(rir, "tal"))])
argv.extend(["-T", f"{fixture_desc(rir)}:{state_dir / 'cache' / 'fixtures' / fixture_name(rir, 'ta')}"])
argv.extend(["-d", str(state_dir / "cache"), str(run_dir)])
prefix = f"env LD_LIBRARY_PATH={shlex.quote(str(remote_root / 'lib'))} /usr/bin/time"
command = (
"set -euo pipefail; "
+ "; ".join(ensure)
+ "; date -u +%Y-%m-%dT%H:%M:%SZ > " + shlex.quote(str(run_dir / "started-at.txt"))
+ "; set +e; "
+ prefix + " -v -o " + shlex.quote(str(run_dir / "process-time.txt"))
+ " -- " + shlex.join(argv)
+ " > " + shlex.quote(str(run_dir / "stdout.log"))
+ " 2> " + shlex.quote(str(run_dir / "stderr.log"))
+ "; ec=$?; set -e; printf '%s\n' \"$ec\" > " + shlex.quote(str(run_dir / "exit-code.txt"))
+ "; date -u +%Y-%m-%dT%H:%M:%SZ > " + shlex.quote(str(run_dir / "finished-at.txt"))
+ "; true"
)
if side["rpKind"] == "rpki-client":
command += (
f"; [ -f {shlex.quote(str(run_dir / 'json'))} ] && cp -f {shlex.quote(str(run_dir / 'json'))} {shlex.quote(str(run_dir / 'report.json'))} || true"
f"; [ -f {shlex.quote(str(run_dir / 'rpki.ccr'))} ] && cp -f {shlex.quote(str(run_dir / 'rpki.ccr'))} {shlex.quote(str(run_dir / 'result.ccr'))} || true"
f"; [ -f {shlex.quote(str(run_dir / 'rpki.cir'))} ] && cp -f {shlex.quote(str(run_dir / 'rpki.cir'))} {shlex.quote(str(run_dir / 'result.cir'))} || true"
)
command += (
f"; python3 - <<'REMOTE_META' {shlex.quote(str(run_dir))} {shlex.quote(side_name)} {shlex.quote(side_label)} {seq} {shlex.quote(sync_mode)}\n"
"import json, pathlib, sys\n"
"run_dir=pathlib.Path(sys.argv[1]); side_name=sys.argv[2]; side_label=sys.argv[3]; seq=int(sys.argv[4]); sync_mode=sys.argv[5]\n"
"def read(p):\n return p.read_text().strip() if p.exists() else None\n"
"def counts_from_report():\n"
" p=run_dir/'report.json'\n"
" if not p.exists():\n"
" return {}\n"
" try:\n"
" report=json.load(open(p))\n"
" except Exception:\n"
" return {}\n"
" meta=report.get('metadata') if isinstance(report, dict) else None\n"
" if isinstance(meta, dict):\n"
" return {'vrps': int(meta.get('vrps') or 0), 'vaps': int(meta.get('vaps') or meta.get('aspas') or 0), 'publicationPoints': int(meta.get('repositories') or 0), 'warnings': 0}\n"
" pps=report.get('publication_points', []) if isinstance(report, dict) else []\n"
" tree=report.get('tree', {}) if isinstance(report, dict) else {}\n"
" pp_warnings=sum(len(pp.get('warnings', [])) for pp in pps if isinstance(pp, dict)) if isinstance(pps, list) else 0\n"
" return {'vrps': len(report.get('vrps', [])), 'vaps': len(report.get('aspas', [])), 'publicationPoints': len(pps) if isinstance(pps, list) else 0, 'warnings': len(tree.get('warnings', [])) + pp_warnings if isinstance(tree, dict) else pp_warnings}\n"
"meta={'sideName':side_name,'sideLabel':side_label,'seq':seq,'syncMode':sync_mode,'startedAt':read(run_dir/'started-at.txt'),'finishedAt':read(run_dir/'finished-at.txt'),'exitCode':int(read(run_dir/'exit-code.txt') or '1'),'counts':counts_from_report()}\n"
"json.dump(meta, open(run_dir/'remote-run-meta.json','w'), indent=2, sort_keys=True); print()\n"
"REMOTE_META"
)
return run_dir, command
def run_remote_sample(ssh_target: str, remote_root: Path, side_name: str, side: dict[str, Any], side_label: str, seq: int, rirs: list[str]) -> Path:
run_dir, command = build_remote_command(remote_root, side_name, side, side_label, seq, rirs)
ssh_script(ssh_target, command)
return run_dir
def append_remote_sequence_item(
ssh_target: str,
remote_root: Path,
side_name: str,
side_label: str,
seq: int,
run_dir: Path,
schedule_mode: str,
) -> dict[str, Any]:
remote_exp_root = remote_root / "experiments" / "sequence"
seq_path = remote_exp_root / ("left-sequence.jsonl" if side_label == "A" else "right-sequence.jsonl")
side_value = "left" if side_label == "A" else "right"
script = f"""
set -euo pipefail
python3 - <<'REMOTE_SEQUENCE_ITEM' {shlex.quote(str(remote_exp_root))} {shlex.quote(str(seq_path))} {shlex.quote(str(run_dir))} {shlex.quote(str(remote_root / 'bin' / 'cir_dump_reject_list'))} {shlex.quote(side_name)} {shlex.quote(side_label)} {seq} {shlex.quote(side_value)} {shlex.quote(schedule_mode)}
import hashlib, json, os, pathlib, subprocess, sys
exp_root=pathlib.Path(sys.argv[1])
seq_path=pathlib.Path(sys.argv[2])
run_dir=pathlib.Path(sys.argv[3])
cir_dump=pathlib.Path(sys.argv[4])
side_name=sys.argv[5]
side_label=sys.argv[6]
seq=int(sys.argv[7])
side_value=sys.argv[8]
schedule_mode=sys.argv[9]
def read(p):
return p.read_text().strip() if p.exists() else None
def sha256_file(p):
h=hashlib.sha256()
with open(p, 'rb') as f:
for chunk in iter(lambda: f.read(1024 * 1024), b''):
h.update(chunk)
return h.hexdigest()
def parse_elapsed_to_ms(raw):
raw=raw.strip()
if not raw:
return 0
if '-' in raw:
days, raw=raw.split('-', 1)
else:
days='0'
parts=raw.split(':')
if len(parts) == 3:
hours, minutes, seconds=parts
elif len(parts) == 2:
hours='0'; minutes, seconds=parts
else:
hours='0'; minutes='0'; seconds=parts[0]
return int(round((int(days)*86400 + int(hours)*3600 + int(minutes)*60 + float(seconds))*1000))
def parse_time_file(p):
data={{}}
if not p.exists():
return data
for line in p.read_text(errors='replace').splitlines():
if 'Elapsed (wall clock) time' in line:
elapsed=line.rsplit('):', 1)[1] if '):' in line else line.rsplit(':', 1)[1]
data['wallMs']=parse_elapsed_to_ms(elapsed)
elif 'Maximum resident set size' in line:
try:
data['maxRssKb']=int(line.rsplit(':', 1)[1].strip())
except ValueError:
pass
return data
def cir_counts(cir):
values={{}}
if not cir.exists():
return {{}}
result=subprocess.run([str(cir_dump), '--cir', str(cir), '--limit', '0'], text=True, capture_output=True, check=True)
for line in result.stdout.splitlines():
if '=' not in line:
continue
key, value=line.split('=', 1)
if key in ('object_count', 'trust_anchor_count', 'reject_count'):
values[key]=int(value)
return {{'cirObjectCount': values.get('object_count', 0), 'cirTrustAnchorCount': values.get('trust_anchor_count', 0), 'cirRejectCount': values.get('reject_count', 0)}}
meta=json.load(open(run_dir/'remote-run-meta.json'))
time_info=parse_time_file(run_dir/'process-time.txt')
counts=dict(meta.get('counts') or {{}})
ccr=run_dir/'result.ccr'
cir=run_dir/'result.cir'
if meta.get('exitCode') != 0:
raise SystemExit(f"remote run failed: exitCode={{meta.get('exitCode')}} runDir={{run_dir}}")
missing=[str(path) for path in (ccr, cir) if not path.exists()]
if missing:
raise SystemExit(f"remote run missing required artifact(s): {{missing}}")
counts.update(cir_counts(cir))
item={{
'schemaVersion': 1,
'rpId': side_name,
'side': side_value,
'seq': seq,
'runId': f'{{side_label}}-{{seq:04d}}',
'syncMode': 'snapshot' if seq == 1 else 'delta',
'status': 'success' if meta.get('exitCode') == 0 else 'failed',
'startTime': meta.get('startedAt'),
'finishTime': meta.get('finishedAt'),
'validationTime': None,
'ccrPath': os.path.relpath(ccr, exp_root),
'cirPath': os.path.relpath(cir, exp_root),
'ccrSha256': sha256_file(ccr) if ccr.exists() else None,
'cirSha256': sha256_file(cir) if cir.exists() else None,
'wallMs': time_info.get('wallMs'),
'maxRssKb': time_info.get('maxRssKb'),
'vrps': counts.get('vrps'),
'vaps': counts.get('vaps'),
'publicationPoints': counts.get('publicationPoints'),
'cirObjectCount': counts.get('cirObjectCount'),
'cirRejectCount': counts.get('cirRejectCount'),
'cirTrustAnchorCount': counts.get('cirTrustAnchorCount'),
'scheduleMode': schedule_mode,
}}
seq_path.parent.mkdir(parents=True, exist_ok=True)
with open(seq_path, 'a', encoding='utf-8') as handle:
handle.write(json.dumps(item, sort_keys=True, ensure_ascii=False) + '\\n')
print(json.dumps(item, sort_keys=True, ensure_ascii=False))
REMOTE_SEQUENCE_ITEM
"""
result = ssh_script(ssh_target, script, capture=True)
lines = [line for line in result.stdout.splitlines() if line.strip()]
if not lines:
raise SystemExit("remote sequence item append produced no output")
return json.loads(lines[-1])
def cleanup_remote_run_nonessential(ssh_target: str, run_dir: Path) -> None:
keep = {
"result.ccr",
"result.cir",
"process-time.txt",
"remote-run-meta.json",
"exit-code.txt",
"started-at.txt",
"finished-at.txt",
"stdout.log",
"stderr.log",
}
keep_json = json.dumps(sorted(keep), ensure_ascii=False)
script = f"""
set -euo pipefail
python3 - <<'REMOTE_CLEAN' {shlex.quote(str(run_dir))} {shlex.quote(keep_json)}
import json, pathlib, sys
run_dir=pathlib.Path(sys.argv[1])
keep=set(json.loads(sys.argv[2]))
removed=0
if run_dir.exists():
for path in run_dir.iterdir():
if path.name in keep or path.is_dir():
continue
try:
removed += path.stat().st_size
path.unlink()
except FileNotFoundError:
pass
print(f'cleaned_nonessential_bytes={{removed}}')
REMOTE_CLEAN
"""
ssh_script(ssh_target, script)
def cir_counts(cir_path: Path) -> dict[str, int]:
result = run_local([str(REPO_ROOT / "target" / "release" / "cir_dump_reject_list"), "--cir", str(cir_path), "--limit", "0"], capture=True)
values: dict[str, int] = {}
for line in result.stdout.splitlines():
if "=" not in line:
continue
key, value = line.split("=", 1)
if key in {"object_count", "trust_anchor_count", "reject_count"}:
values[key] = int(value)
return {
"cirObjectCount": values.get("object_count", 0),
"cirTrustAnchorCount": values.get("trust_anchor_count", 0),
"cirRejectCount": values.get("reject_count", 0),
}
def report_counts(path: Path, rp_kind: str) -> dict[str, int]:
if not path.is_file():
return {}
report = load_json(path)
if rp_kind == "rpki-client":
meta = report.get("metadata", {})
return {
"vrps": int(meta.get("vrps", 0)),
"vaps": int(meta.get("vaps", 0) or meta.get("aspas", 0) or 0),
"publicationPoints": int(meta.get("repositories", 0)),
"warnings": 0,
}
pps = report.get("publication_points", [])
tree = report.get("tree", {})
return {
"vrps": len(report.get("vrps", [])),
"vaps": len(report.get("aspas", [])),
"publicationPoints": len(pps),
"warnings": len(tree.get("warnings", [])) + sum(len(pp.get("warnings", [])) for pp in pps if isinstance(pp, dict)),
}
def build_sequence_item(local_root: Path, side_name: str, side_label: str, side: dict[str, Any], seq: int, run_dir: Path) -> dict[str, Any]:
ccr = run_dir / "result.ccr"
cir = run_dir / "result.cir"
meta = load_json(run_dir / "remote-run-meta.json")
time_info = parse_time_file(run_dir / "process-time.txt")
counts = dict(meta.get("counts") or {})
if not counts:
counts = report_counts(run_dir / "report.json", side["rpKind"])
counts.update(cir_counts(cir))
return {
"schemaVersion": 1,
"rpId": side_name,
"side": "left" if side_label == "A" else "right",
"seq": seq,
"runId": f"{side_label}-{seq:04d}",
"syncMode": "snapshot" if seq == 1 else "delta",
"status": "success" if meta.get("exitCode") == 0 else "failed",
"startTime": meta.get("startedAt"),
"finishTime": meta.get("finishedAt"),
"validationTime": None,
"ccrPath": ccr.relative_to(local_root).as_posix(),
"cirPath": cir.relative_to(local_root).as_posix(),
"ccrSha256": sha256_file(ccr),
"cirSha256": sha256_file(cir),
"wallMs": time_info.get("wallMs"),
"maxRssKb": time_info.get("maxRssKb"),
"vrps": counts.get("vrps"),
"vaps": counts.get("vaps"),
"publicationPoints": counts.get("publicationPoints"),
"cirObjectCount": counts.get("cirObjectCount"),
"cirRejectCount": counts.get("cirRejectCount"),
"cirTrustAnchorCount": counts.get("cirTrustAnchorCount"),
}
def run_sequence_triage(local_exp_root: Path, args: argparse.Namespace) -> None:
compare_dir = local_exp_root / "sequence-triage"
run_local([
str(REPO_ROOT / "target" / "release" / "sequence_triage_ccr_cir"),
"--left-sequence", str(local_exp_root / "left-sequence.jsonl"),
"--right-sequence", str(local_exp_root / "right-sequence.jsonl"),
"--out-dir", str(compare_dir),
"--align-window-runs", str(args.align_window_runs),
"--align-window-secs", str(args.align_window_secs),
"--sample-limit", str(args.sample_limit),
"--timeline-sample-limit", str(args.timeline_sample_limit),
])
def run_sequence_triage_remote(ssh_target: str, remote_root: Path, args: argparse.Namespace) -> None:
remote_exp_root = remote_root / "experiments" / "sequence"
compare_dir = remote_exp_root / "sequence-triage"
time_path = remote_exp_root / "sequence-triage-time.txt"
command = " ".join(
shlex.quote(item)
for item in [
str(remote_root / "bin" / "sequence_triage_ccr_cir"),
"--left-sequence", str(remote_exp_root / "left-sequence.jsonl"),
"--right-sequence", str(remote_exp_root / "right-sequence.jsonl"),
"--out-dir", str(compare_dir),
"--align-window-runs", str(args.align_window_runs),
"--align-window-secs", str(args.align_window_secs),
"--sample-limit", str(args.sample_limit),
"--timeline-sample-limit", str(args.timeline_sample_limit),
]
)
ssh_script(
ssh_target,
"set -euo pipefail; "
f"rm -rf {shlex.quote(str(compare_dir))} {shlex.quote(str(time_path))}; "
f"/usr/bin/time -v -o {shlex.quote(str(time_path))} -- {command}",
)
def sync_side_to_analysis_remote(
source_ssh_target: str,
source_remote_root: Path,
analysis_ssh_target: str,
analysis_remote_root: Path,
side_label: str,
) -> None:
source_exp_root = source_remote_root / "experiments" / "sequence"
analysis_exp_root = analysis_remote_root / "experiments" / "sequence"
sequence_name = "left-sequence.jsonl" if side_label == "A" else "right-sequence.jsonl"
if same_remote_location(source_ssh_target, source_exp_root, analysis_ssh_target, analysis_exp_root):
return
side_dir = source_exp_root / side_label
script = (
"set -euo pipefail; "
f"ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new {shlex.quote(analysis_ssh_target)} "
f"{shlex.quote('mkdir -p ' + shlex.quote(str(analysis_exp_root / side_label)))}; "
f"rsync -az --delete {shlex.quote(str(side_dir))}/ {shlex.quote(analysis_ssh_target)}:{shlex.quote(str(analysis_exp_root / side_label))}/; "
f"rsync -az {shlex.quote(str(source_exp_root / sequence_name))} "
f"{shlex.quote(analysis_ssh_target)}:{shlex.quote(str(analysis_exp_root / sequence_name))}"
)
ssh_script(source_ssh_target, script)
def run_side_sequence(
args: argparse.Namespace,
ssh_target: str,
remote_root: Path,
local_exp_root: Path,
side_label: str,
side_name: str,
side: dict[str, Any],
seq_path: Path,
rirs: list[str],
) -> list[dict[str, Any]]:
side_progress: list[dict[str, Any]] = []
for seq in range(1, args.samples_per_side + 1):
side_progress.append(
run_one_side_sample(args, ssh_target, remote_root, local_exp_root, side_label, side_name, side, seq_path, seq, rirs)
)
return side_progress
def run_one_side_sample(
args: argparse.Namespace,
ssh_target: str,
remote_root: Path,
local_exp_root: Path,
side_label: str,
side_name: str,
side: dict[str, Any],
seq_path: Path,
seq: int,
rirs: list[str],
) -> dict[str, Any]:
rir_label = ",".join(rirs)
print(
f"[run] {side_label} {side_name} seq={seq} rirs={rir_label} schedule={args.schedule_mode}",
flush=True,
)
remote_run_dir = run_remote_sample(ssh_target, remote_root, side_name, side, side_label, seq, rirs)
if args.remote_triage:
item = append_remote_sequence_item(
ssh_target,
remote_root,
side_name,
side_label,
seq,
remote_run_dir,
args.schedule_mode,
)
if args.cleanup_run_nonessential:
cleanup_remote_run_nonessential(ssh_target, remote_run_dir)
else:
local_run_dir = local_exp_root / side_label / f"run_{seq:04d}"
rsync_run_artifacts_from_remote(ssh_target, remote_run_dir, local_run_dir)
item = build_sequence_item(local_exp_root, side_name, side_label, side, seq, local_run_dir)
item["scheduleMode"] = args.schedule_mode
append_jsonl(seq_path, item)
print(
f"[done] {side_label} seq={seq} wallMs={item.get('wallMs')} vrps={item.get('vrps')} vaps={item.get('vaps')} objects={item.get('cirObjectCount')} rejects={item.get('cirRejectCount')}",
flush=True,
)
return item
def run_experiment(args: argparse.Namespace) -> None:
if not args.skip_build:
build_tool_binaries()
rirs = parse_rirs(args.rirs)
left = side_config(args.left)
right = side_config(args.right)
run_root = Path(args.run_root).resolve()
remote_root = Path(args.remote_root)
left_ssh_target = args.left_ssh_target or args.ssh_target
right_ssh_target = args.right_ssh_target or args.ssh_target
analysis_ssh_target = args.analysis_ssh_target or left_ssh_target
left_remote_root = Path(args.left_remote_root or args.remote_root)
right_remote_root = Path(args.right_remote_root or args.remote_root)
analysis_remote_root = Path(args.analysis_remote_root or args.remote_root)
run_root.mkdir(parents=True, exist_ok=True)
write_json(run_root / "experiment-config.json", {
"schemaVersion": 1,
"generatedAtUtc": utc_stamp(),
"left": args.left,
"right": args.right,
"samplesPerSide": args.samples_per_side,
"rirs": rirs,
"scheduleMode": args.schedule_mode,
"remoteRoot": str(remote_root),
"leftSshTarget": left_ssh_target,
"rightSshTarget": right_ssh_target,
"analysisSshTarget": analysis_ssh_target,
"leftRemoteRoot": str(left_remote_root),
"rightRemoteRoot": str(right_remote_root),
"analysisRemoteRoot": str(analysis_remote_root),
"sshTarget": args.ssh_target,
})
if args.dry_run:
print(json.dumps(load_json(run_root / "experiment-config.json"), indent=2, ensure_ascii=False))
return
if args.triage_only:
run_sequence_triage(run_root / "experiments" / "sequence", args)
print(json.dumps({
"runRoot": str(run_root),
"triage": str(run_root / "experiments" / "sequence" / "sequence-triage" / "sequence-triage.json"),
}, indent=2))
return
prepared_remotes: dict[tuple[str, str], bool] = {}
prepare_remote_once(prepared_remotes, left_ssh_target, left_remote_root, needs_rpki_client=(left["rpKind"] == "rpki-client"))
prepare_remote_once(prepared_remotes, right_ssh_target, right_remote_root, needs_rpki_client=(right["rpKind"] == "rpki-client"))
prepare_remote_once(prepared_remotes, analysis_ssh_target, analysis_remote_root, needs_rpki_client=False)
local_exp_root = run_root / "experiments" / "sequence"
left_seq_path = local_exp_root / "left-sequence.jsonl"
right_seq_path = local_exp_root / "right-sequence.jsonl"
left_seq_path.unlink(missing_ok=True)
right_seq_path.unlink(missing_ok=True)
progress: list[dict[str, Any]] = []
if args.schedule_mode == "interleaved":
for seq in range(1, args.samples_per_side + 1):
for side_label, ssh_target, side_remote_root, side_name, side, seq_path in [
("A", left_ssh_target, left_remote_root, args.left, left, left_seq_path),
("B", right_ssh_target, right_remote_root, args.right, right, right_seq_path),
]:
progress.append(
run_one_side_sample(
args,
ssh_target,
side_remote_root,
local_exp_root,
side_label,
side_name,
side,
seq_path,
seq,
rirs,
)
)
else:
with ThreadPoolExecutor(max_workers=2) as executor:
futures = [
executor.submit(run_side_sequence, args, left_ssh_target, left_remote_root, local_exp_root, "A", args.left, left, left_seq_path, rirs),
executor.submit(run_side_sequence, args, right_ssh_target, right_remote_root, local_exp_root, "B", args.right, right, right_seq_path, rirs),
]
for future in as_completed(futures):
progress.extend(future.result())
progress.sort(key=lambda item: (str(item.get("side")), int(item.get("seq") or 0)))
write_json(local_exp_root / "run-progress.json", progress)
if args.remote_triage:
sync_side_to_analysis_remote(left_ssh_target, left_remote_root, analysis_ssh_target, analysis_remote_root, "A")
sync_side_to_analysis_remote(right_ssh_target, right_remote_root, analysis_ssh_target, analysis_remote_root, "B")
remote_exp_root = analysis_remote_root / "experiments" / "sequence"
remote_progress = json.dumps(progress, sort_keys=True, ensure_ascii=False)
ssh_script(
analysis_ssh_target,
"set -euo pipefail; "
f"cat > {shlex.quote(str(remote_exp_root / 'run-progress.json'))} <<'REMOTE_PROGRESS_JSON'\n"
f"{remote_progress}\n"
"REMOTE_PROGRESS_JSON\n",
)
run_sequence_triage_remote(analysis_ssh_target, analysis_remote_root, args)
if args.fetch_remote_analysis:
rsync_remote_analysis_from_remote(analysis_ssh_target, remote_exp_root, local_exp_root)
else:
run_sequence_triage(local_exp_root, args)
ssh_script(analysis_ssh_target, f"df -h /data / > {shlex.quote(str(analysis_remote_root / 'df-after.txt'))} 2>&1 || true; free -h > {shlex.quote(str(analysis_remote_root / 'free-after.txt'))} 2>&1 || true")
compare_dir = local_exp_root / "sequence-triage"
remote_compare_dir = analysis_remote_root / "experiments" / "sequence" / "sequence-triage"
print(json.dumps({
"runRoot": str(run_root),
"remoteRoot": str(remote_root),
"leftRemoteRoot": str(left_remote_root),
"rightRemoteRoot": str(right_remote_root),
"analysisRemoteRoot": str(analysis_remote_root),
"triage": str(compare_dir / "sequence-triage.json") if not args.remote_triage or args.fetch_remote_analysis else None,
"remoteTriage": str(remote_compare_dir / "sequence-triage.json") if args.remote_triage else None,
}, indent=2))
def main() -> None:
parser = argparse.ArgumentParser(description="Feature #043 all5 sequence triage experiment driver")
parser.add_argument("--run-root", required=True)
parser.add_argument("--remote-root", required=True)
parser.add_argument("--ssh-target", default=os.environ.get("SSH_TARGET", "root@47.251.56.108"))
parser.add_argument("--left-ssh-target")
parser.add_argument("--right-ssh-target")
parser.add_argument("--analysis-ssh-target")
parser.add_argument("--left-remote-root")
parser.add_argument("--right-remote-root")
parser.add_argument("--analysis-remote-root")
parser.add_argument("--left", default="ours-standard")
parser.add_argument("--right", default="rpki-client-standard")
parser.add_argument("--samples-per-side", type=int, default=3)
parser.add_argument("--rirs", default=",".join(DEFAULT_RIRS))
parser.add_argument("--schedule-mode", choices=["interleaved", "parallel"], default="interleaved")
parser.add_argument("--align-window-runs", type=int, default=2)
parser.add_argument("--align-window-secs", type=int, default=1800)
parser.add_argument("--sample-limit", type=int, default=200)
parser.add_argument("--timeline-sample-limit", type=int, default=0)
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--skip-build", action="store_true", help="reuse existing release binaries")
parser.add_argument("--triage-only", action="store_true", help="only rerun local sequence triage for an existing run root")
parser.add_argument("--remote-triage", action="store_true", help="keep CIR/CCR on remote, write sequence JSONL remotely, and run triage on remote")
parser.add_argument("--fetch-remote-analysis", action="store_true", help="when --remote-triage is set, fetch only small sequence/triage JSON outputs; never fetch CIR/CCR")
parser.add_argument("--cleanup-run-nonessential", action="store_true", help="after each successful remote sequence item, remove report/log/CSV files and keep only CIR/CCR/timing/meta")
args = parser.parse_args()
if args.samples_per_side < 2:
raise SystemExit("--samples-per-side must be >= 2")
run_experiment(args)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,265 @@
#!/usr/bin/env bash
set -euo pipefail
REMOTE_HOST="${REMOTE_HOST:-root@43.110.128.200}"
REMOTE_CONFIG="${REMOTE_CONFIG:-/root/inter-rp-runners/inter-rp.env}"
REMOTE_SCRIPTS_DIR="${REMOTE_SCRIPTS_DIR:-/root/inter-rp-runners/scripts}"
DEFAULT_RETAIN_RUNS="${RETAIN_RUNS:-20}"
DEFAULT_RSS_SAMPLE_MS="${RSS_SAMPLE_MS:-500}"
usage() {
cat <<'USAGE'
Usage:
control_remote200_rp.sh status [rpki-client|routinator|all]
control_remote200_rp.sh start <rpki-client|routinator>
control_remote200_rp.sh stop <rpki-client|routinator|all>
control_remote200_rp.sh restart <rpki-client|routinator>
Environment overrides:
REMOTE_HOST=root@43.110.128.200
REMOTE_CONFIG=/root/inter-rp-runners/inter-rp.env
REMOTE_SCRIPTS_DIR=/root/inter-rp-runners/scripts
RETAIN_RUNS=20
RSS_SAMPLE_MS=500
Notes:
- start uses the remote run_single_rp_with_rss.sh loop and runs until stopped.
- stop only kills the selected RP loop branch and its current child processes.
- rpki-client and routinator are managed independently.
USAGE
}
ACTION="${1:-status}"
RP="${2:-all}"
case "$ACTION" in
status|start|stop|restart) ;;
-h|--help|help)
usage
exit 0
;;
*)
echo "unknown action: $ACTION" >&2
usage >&2
exit 2
;;
esac
case "$RP" in
rpki-client|routinator|all) ;;
*)
echo "unknown RP: $RP" >&2
usage >&2
exit 2
;;
esac
if [[ "$ACTION" == "start" || "$ACTION" == "restart" ]] && [[ "$RP" == "all" ]]; then
echo "start/restart requires one RP: rpki-client or routinator" >&2
exit 2
fi
ssh "$REMOTE_HOST" \
"REMOTE_CONFIG='$REMOTE_CONFIG' REMOTE_SCRIPTS_DIR='$REMOTE_SCRIPTS_DIR' RETAIN_RUNS='$DEFAULT_RETAIN_RUNS' RSS_SAMPLE_MS='$DEFAULT_RSS_SAMPLE_MS' ACTION='$ACTION' RP='$RP' bash -s" <<'REMOTE'
set -euo pipefail
load_config() {
if [[ -f "$REMOTE_CONFIG" ]]; then
# shellcheck disable=SC1090
source "$REMOTE_CONFIG"
fi
INTER_RP_ROOT="${INTER_RP_ROOT:-/var/lib/inter-rp-runners}"
RETAIN_RUNS="${RETAIN_RUNS:-20}"
RSS_SAMPLE_MS="${RSS_SAMPLE_MS:-500}"
ROUTINATOR_RUN_COMMAND="${ROUTINATOR_RUN_COMMAND:-/root/inter-rp-runners/scripts/run_routinator_once.sh}"
RPKI_CLIENT_RUN_COMMAND="${RPKI_CLIENT_RUN_COMMAND:-/root/inter-rp-runners/scripts/run_rpki_client_official_98_once.sh}"
}
rp_root_name() {
case "$1" in
routinator) echo "routinator" ;;
rpki-client) echo "rpki-client" ;;
esac
}
rp_pid_file() {
echo "$INTER_RP_ROOT/$(rp_root_name "$1").loop.pid"
}
rp_log_file() {
echo "$INTER_RP_ROOT/$(rp_root_name "$1").loop.log"
}
rp_command() {
case "$1" in
routinator) echo "$ROUTINATOR_RUN_COMMAND" ;;
rpki-client) echo "$RPKI_CLIENT_RUN_COMMAND" ;;
esac
}
rp_binary_pattern() {
case "$1" in
routinator) echo "/root/inter-rp-runners/bin/routinator|[[:space:]]routinator[[:space:]]" ;;
rpki-client) echo "rpki-client-official-9\\.8|[[:space:]]rpki-client[[:space:]]" ;;
esac
}
loop_pattern() {
local rp="$1"
echo "run_single_rp_with_rss\\.sh --rp $rp "
}
is_running_pid() {
local pid="${1:-}"
[[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null
}
current_loop_pids() {
local rp="$1"
pgrep -af "$(loop_pattern "$rp")" 2>/dev/null | awk '{print $1}' || true
}
status_one() {
local rp="$1"
local pid_file
pid_file="$(rp_pid_file "$rp")"
local pid=""
[[ -f "$pid_file" ]] && pid="$(cat "$pid_file" 2>/dev/null || true)"
echo "== $rp =="
if is_running_pid "$pid"; then
echo "loop: running pid=$pid"
else
local detected
detected="$(current_loop_pids "$rp" | paste -sd, -)"
if [[ -n "$detected" ]]; then
echo "loop: running detected_pids=$detected"
else
echo "loop: stopped"
fi
fi
ps -eo pid,ppid,stat,etime,%cpu,%mem,rss,cmd --sort=-%cpu \
| grep -E "$(loop_pattern "$rp")|$(rp_binary_pattern "$rp")" \
| grep -v grep \
| head -20 || true
local latest="$INTER_RP_ROOT/$(rp_root_name "$rp")/latest/run-meta.json"
if [[ -f "$latest" ]]; then
python3 - "$latest" <<'PY'
import json, sys
path = sys.argv[1]
with open(path, "r", encoding="utf-8") as handle:
meta = json.load(handle)
print("latest:", "runSeq=", meta.get("runSeq"), "success=", meta.get("success"), "wallMs=", meta.get("wallMs"), "vrps=", meta.get("counts", {}).get("vrps"), "vaps=", meta.get("counts", {}).get("vaps"), "rssKb=", meta.get("maxRssKb", {}).get("aggregatePeak"))
PY
else
echo "latest: none"
fi
}
start_one() {
local rp="$1"
local pid_file log_file root_name run_command
pid_file="$(rp_pid_file "$rp")"
log_file="$(rp_log_file "$rp")"
root_name="$(rp_root_name "$rp")"
run_command="$(rp_command "$rp")"
mkdir -p "$INTER_RP_ROOT/$root_name" "$INTER_RP_ROOT"
local existing=""
if [[ -f "$pid_file" ]]; then
existing="$(cat "$pid_file" 2>/dev/null || true)"
fi
if is_running_pid "$existing"; then
echo "$rp already running pid=$existing"
return 0
fi
local detected
detected="$(current_loop_pids "$rp" | head -1)"
if [[ -n "$detected" ]] && is_running_pid "$detected"; then
echo "$detected" >"$pid_file"
echo "$rp already running detected pid=$detected"
return 0
fi
nohup bash -lc '
set -euo pipefail
while true; do
"'"$REMOTE_SCRIPTS_DIR"'/run_single_rp_with_rss.sh" \
--rp "'"$rp"'" \
--root "'"$INTER_RP_ROOT/$root_name"'" \
--command "'"$run_command"'" \
--retain-runs "'"$RETAIN_RUNS"'" \
--sample-ms "'"$RSS_SAMPLE_MS"'" || true
done
' >"$log_file" 2>&1 &
local pid="$!"
echo "$pid" >"$pid_file"
echo "$rp started pid=$pid log=$log_file"
}
stop_one() {
local rp="$1"
local candidates=()
local pid_file pid
pid_file="$(rp_pid_file "$rp")"
if [[ -f "$pid_file" ]]; then
pid="$(cat "$pid_file" 2>/dev/null || true)"
[[ -n "$pid" ]] && candidates+=("$pid")
fi
while read -r pid; do
[[ -n "$pid" ]] && candidates+=("$pid")
done < <(current_loop_pids "$rp")
while read -r pid; do
[[ -n "$pid" ]] && candidates+=("$pid")
done < <(pgrep -af "$(rp_binary_pattern "$rp")" 2>/dev/null | awk '{print $1}' || true)
if ((${#candidates[@]} == 0)); then
echo "$rp already stopped"
rm -f "$pid_file"
return 0
fi
local unique_pids
unique_pids="$(printf '%s\n' "${candidates[@]}" | awk '!seen[$0]++' | tr '\n' ' ')"
echo "stopping $rp pids=$unique_pids"
for pid in $unique_pids; do
kill -TERM "$pid" 2>/dev/null || true
done
sleep 3
for pid in $unique_pids; do
kill -KILL "$pid" 2>/dev/null || true
done
rm -f "$pid_file"
echo "$rp stopped"
}
load_config
case "$ACTION:$RP" in
status:all)
date -Is
uptime
status_one routinator
status_one rpki-client
;;
status:*)
date -Is
uptime
status_one "$RP"
;;
start:*)
start_one "$RP"
status_one "$RP"
;;
stop:all)
stop_one routinator
stop_one rpki-client
;;
stop:*)
stop_one "$RP"
status_one "$RP"
;;
restart:*)
stop_one "$RP"
start_one "$RP"
status_one "$RP"
;;
esac
REMOTE

View File

@@ -0,0 +1,26 @@
# 远端200 runner 配置示例。
# 该脚本只负责任务调度、RSS采样、run目录和run-meta.json;具体RP命令由下面两个变量提供。
# 远端200上的运行根目录。
# 建议把运行态数据放到 /var/lib,避免 rpki-client 降权后无法读写 /root。
INTER_RP_ROOT=/var/lib/inter-rp-runners
# -1 表示持续运行;正整数表示每个RP运行多少轮。
MAX_RUNS=-1
# 每轮结束后的等待秒数。
RUN_INTERVAL_SECS=0
# 每个RP保留最近多少轮。
RETAIN_RUNS=20
# RSS采样间隔,毫秒。
RSS_SAMPLE_MS=500
# Routinator 命令模板。命令运行时会自动导出 RUN_DIR/RP_ROOT/RUN_SEQ/RUN_ID/RP_NAME。
# 命令必须把 vrps.csv/vaps.csv 写入 $RUN_DIR;Routinator 使用 --enable-aspa + JSON 输出转换出 VAP CSV。
ROUTINATOR_RUN_COMMAND=/root/inter-rp-runners/scripts/run_routinator_once.sh
# 官方 rpki-client 9.8 命令模板。
# rpki-client 会降权运行,cache 和 output 必须预先放在可写目录;命令需复制 output/csv、output/rpki.ccr,并从 output/json 转换出 vaps.csv。
RPKI_CLIENT_RUN_COMMAND=/root/inter-rp-runners/scripts/run_rpki_client_official_98_once.sh

View File

@@ -0,0 +1,472 @@
#!/usr/bin/env python3
import csv
import datetime as dt
import json
import os
import re
import socket
import sys
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
RUN_ROOT = Path(os.environ.get("OURS_RUN_ROOT", "/root/ours-rp-continuous/portable-soak"))
PEER_ROOT = Path(os.environ.get("PEER_ROOT", "/root/ours-rp-continuous/portable-soak/inter-rp-peers"))
INSTANCE = os.environ.get("INTER_RP_INSTANCE", "remote200-inter-rp")
LISTEN = os.environ.get("INTER_RP_LISTEN", "0.0.0.0:9557")
SCAN_TTL = float(os.environ.get("INTER_RP_SCAN_TTL_SECONDS", "10"))
_cache_lock = threading.Lock()
_cache = {"deadline": 0.0, "metrics": "", "status": {}}
_count_cache = {}
_set_cache = {}
def unix_now():
return time.time()
def parse_rfc3339(value):
if not value:
return None
text = str(value).replace("Z", "+00:00")
try:
return dt.datetime.fromisoformat(text).timestamp()
except Exception:
return None
def read_json(path):
with open(path, "r", encoding="utf-8") as handle:
return json.load(handle)
def latest_ours_run():
runs = RUN_ROOT / "runs"
candidates = sorted(p for p in runs.glob("run_*") if (p / "run-summary.json").exists())
return candidates[-1] if candidates else None
def unique_csv_set(path, cols):
if not path.exists():
return None
stat = path.stat()
key = (str(path), stat.st_mtime_ns, stat.st_size, cols)
if key in _set_cache:
return _set_cache[key]
seen = set()
with open(path, "r", encoding="utf-8", newline="") as handle:
reader = csv.reader(handle)
first = True
for row in reader:
if first:
first = False
continue
if not row:
continue
if len(row) < cols:
continue
seen.add(tuple(cell.strip() for cell in row[:cols]))
_set_cache.clear()
_set_cache[key] = seen
return seen
def count_unique_csv(path, cols):
values = unique_csv_set(path, cols)
if values is None:
return None
return len(values)
def metric_line(name, labels, value):
label_text = ",".join(f'{k}="{str(v).replace(chr(92), chr(92)+chr(92)).replace(chr(34), chr(92)+chr(34))}"' for k, v in labels.items())
return f"{name}{{{label_text}}} {value}\n"
def bool_num(value):
return 1 if value else 0
def sample_ours(now, errors):
sample = {"rp": "ours-rp", "present": False, "success": False, "max": {}, "errors": 0}
run_dir = latest_ours_run()
if run_dir is None:
sample["errors"] += 1
errors.append("ours-rp: no run-summary.json")
return sample
sample["present"] = True
summary_path = run_dir / "run-summary.json"
try:
summary = read_json(summary_path)
except Exception as exc:
sample["errors"] += 1
errors.append(f"ours-rp: read {summary_path}: {exc}")
return sample
sample["run_id"] = summary.get("runId") or run_dir.name
sample["run_seq"] = summary.get("runSeq") or int(run_dir.name.split("_")[-1])
sample["success"] = summary.get("status") == "success" and int(summary.get("exitCode", 0)) == 0
if summary.get("wallMs") is not None:
sample["wall"] = float(summary["wallMs"]) / 1000.0
finished = parse_rfc3339(summary.get("finishedAtRfc3339Utc"))
if finished is not None:
sample["finished"] = finished
sample["age"] = max(0.0, now - finished)
rss = summary.get("processMetrics", {}).get("maxRssKb")
if rss is not None:
sample["max"]["parent"] = int(rss) * 1024
sample["max"]["aggregate_peak"] = int(rss) * 1024
sample["vrp_set"] = unique_csv_set(run_dir / "vrps.csv", 3)
sample["vap_set"] = unique_csv_set(run_dir / "vaps.csv", 2)
sample["vrps"] = len(sample["vrp_set"]) if sample["vrp_set"] is not None else None
sample["vaps"] = len(sample["vap_set"]) if sample["vap_set"] is not None else None
return sample
def sample_routinator(now, errors):
rp = "routinator"
latest = PEER_ROOT / rp / "latest"
sample = {"rp": rp, "present": False, "success": False, "max": {}, "errors": 0}
if not latest.exists():
sample["errors"] += 1
errors.append(f"routinator: missing latest directory: {latest}")
return sample
sample["present"] = True
meta_path = latest / "run-meta.json"
try:
meta = read_json(meta_path)
except Exception as exc:
sample["errors"] += 1
errors.append(f"routinator: read {meta_path}: {exc}")
return sample
sample["run_id"] = meta.get("runId")
sample["run_seq"] = meta.get("runSeq")
sample["success"] = bool(meta.get("success"))
if meta.get("wallMs") is not None:
sample["wall"] = float(meta["wallMs"]) / 1000.0
finished = parse_rfc3339(meta.get("finishedAtRfc3339Utc"))
if finished is not None:
sample["finished"] = finished
sample["age"] = max(0.0, now - finished)
max_rss = meta.get("maxRssKb", {})
for label, key in [("parent", "parent"), ("child_max", "childMax"), ("aggregate_peak", "aggregatePeak")]:
if max_rss.get(key) is not None:
sample["max"][label] = int(max_rss[key]) * 1024
sample["vrp_set"] = unique_csv_set(latest / "vrps.csv", 3)
sample["vap_set"] = unique_csv_set(latest / "vaps.csv", 2)
sample["vrps"] = len(sample["vrp_set"]) if sample["vrp_set"] is not None else None
sample["vaps"] = len(sample["vap_set"]) if sample["vap_set"] is not None else None
return sample
def parse_prom_labels(label_text):
labels = {}
for key, value in re.findall(r'(\w+)="((?:[^"\\]|\\.)*)"', label_text):
labels[key] = value.replace(r'\"', '"').replace(r'\\', '\\')
return labels
def parse_prometheus_samples(text, metric_name):
pattern = re.compile(r'^' + re.escape(metric_name) + r'\{([^}]*)\}\s+([-+0-9.eE]+)\s*$', re.MULTILINE)
for match in pattern.finditer(text):
try:
value = float(match.group(2))
except ValueError:
continue
yield parse_prom_labels(match.group(1)), value
def canonical_object_type(value):
value = (value or "").strip().lower()
if value in ("manifest", "mft"):
return "mft"
if value == "crl":
return "crl"
if value in ("certificate", "cert", "ca_cert", "router_cert", "ee_cert", "crt"):
return "crt"
if value == "roa":
return "roa"
if value == "aspa":
return "aspa"
return None
def empty_object_counts():
return {"mft": 0, "crl": 0, "crt": 0, "roa": 0, "aspa": 0}
def add_object_count(counts_by_uri, uri, object_type, value):
canonical = canonical_object_type(object_type)
if not uri or canonical is None:
return
counts = counts_by_uri.setdefault(uri, empty_object_counts())
counts[canonical] = counts.get(canonical, 0) + int(value)
def object_count_labels(counts_by_uri, uri):
counts = counts_by_uri.get(uri, empty_object_counts())
return {key: str(int(counts.get(key, 0))) for key in ["mft", "crl", "crt", "roa", "aspa"]}
def load_ours_repo_sets(errors):
try:
import urllib.request
text = urllib.request.urlopen("http://127.0.0.1:9556/metrics", timeout=10).read().decode("utf-8", "replace")
except Exception as exc:
errors.append(f"ours-rp repo metrics: {exc}")
return {"total": set(), "available": set(), "failed": set(), "info": {}, "object_counts": {}}
info = {}
states = {}
object_counts = {}
for labels, value in parse_prometheus_samples(text, "ours_rp_repository_info"):
uri = labels.get("uri")
if uri:
info[uri] = labels
for labels, value in parse_prometheus_samples(text, "ours_rp_repository_terminal_state_publication_points"):
uri = None
repo_id = labels.get("repo_id")
for candidate_uri, candidate in info.items():
if candidate.get("repo_id") == repo_id:
uri = candidate_uri
break
if not uri:
continue
states.setdefault(uri, {})[labels.get("terminal_state", "unknown")] = value
for labels, value in parse_prometheus_samples(text, "ours_rp_repository_objects_by_type"):
uri = labels.get("uri")
if not uri:
repo_id = labels.get("repo_id")
for candidate_uri, candidate in info.items():
if candidate.get("repo_id") == repo_id:
uri = candidate_uri
break
add_object_count(object_counts, uri, labels.get("object_type"), value)
total = set(info)
available = set()
failed = set()
for uri in total:
repo_states = states.get(uri, {})
non_failed = sum(value for state, value in repo_states.items() if state != "failed_no_cache")
if non_failed > 0:
available.add(uri)
else:
failed.add(uri)
return {"total": total, "available": available, "failed": failed, "info": info, "object_counts": object_counts}
def load_routinator_repo_sets(errors):
path = PEER_ROOT / "routinator" / "routinator-metrics.prom"
if not path.exists():
errors.append(f"routinator repo metrics: missing {path}")
return {"total": set(), "success": set(), "failed": set(), "duration": {}, "object_counts": {}}
try:
text = path.read_text(encoding="utf-8", errors="replace")
except Exception as exc:
errors.append(f"routinator repo metrics: read {path}: {exc}")
return {"total": set(), "success": set(), "failed": set(), "duration": {}, "object_counts": {}}
total = set()
success = set()
failed = set()
duration = {}
object_counts = {}
publication_point_states = {}
for metric in ["routinator_rrdp_status", "routinator_rsync_status"]:
for labels, value in parse_prometheus_samples(text, metric):
uri = labels.get("uri")
if not uri:
continue
total.add(uri)
ok = value in (0.0, 200.0, 304.0)
if ok:
success.add(uri)
else:
failed.add(uri)
for labels, value in parse_prometheus_samples(text, "routinator_repository_publication_points_total"):
uri = labels.get("uri")
state = labels.get("state", "unknown")
if not uri:
continue
total.add(uri)
publication_point_states.setdefault(uri, {})[state] = publication_point_states.setdefault(uri, {}).get(state, 0.0) + value
for uri, states in publication_point_states.items():
valid_count = states.get("valid", 0.0)
non_valid_count = sum(value for state, value in states.items() if state != "valid")
if valid_count > 0:
success.add(uri)
elif non_valid_count > 0:
failed.add(uri)
for metric in ["routinator_rrdp_duration", "routinator_rsync_duration"]:
for labels, value in parse_prometheus_samples(text, metric):
uri = labels.get("uri")
if uri:
duration[uri] = max(duration.get(uri, 0.0), value)
for labels, value in parse_prometheus_samples(text, "routinator_repository_objects_total"):
add_object_count(object_counts, labels.get("uri"), labels.get("type"), value)
failed = failed - success
return {"total": total, "success": success, "failed": failed, "duration": duration, "object_counts": object_counts}
def emit_repo_diff_metrics(out, errors):
ours = load_ours_repo_sets(errors)
routinator = load_routinator_repo_sets(errors)
ours_ok = ours["available"]
rout_ok = routinator["success"]
only_ours = sorted(ours_ok - rout_ok)
only_routinator = sorted(rout_ok - ours_ok)
both = ours_ok & rout_ok
neither = (ours["total"] | routinator["total"]) - (ours_ok | rout_ok)
base = {"instance": INSTANCE, "left": "ours-rp", "right": "routinator"}
out.append(metric_line("inter_rp_repo_sync_total", {"instance": INSTANCE, "rp": "ours-rp", "state": "total"}, len(ours["total"])))
out.append(metric_line("inter_rp_repo_sync_total", {"instance": INSTANCE, "rp": "ours-rp", "state": "available"}, len(ours_ok)))
out.append(metric_line("inter_rp_repo_sync_total", {"instance": INSTANCE, "rp": "ours-rp", "state": "failed"}, len(ours["failed"])))
out.append(metric_line("inter_rp_repo_sync_total", {"instance": INSTANCE, "rp": "routinator", "state": "total"}, len(routinator["total"])))
out.append(metric_line("inter_rp_repo_sync_total", {"instance": INSTANCE, "rp": "routinator", "state": "available"}, len(rout_ok)))
out.append(metric_line("inter_rp_repo_sync_total", {"instance": INSTANCE, "rp": "routinator", "state": "failed"}, len(routinator["failed"])))
out.append(metric_line("inter_rp_repo_sync_overlap_total", {**base, "class": "both_available"}, len(both)))
out.append(metric_line("inter_rp_repo_sync_overlap_total", {**base, "class": "only_ours"}, len(only_ours)))
out.append(metric_line("inter_rp_repo_sync_overlap_total", {**base, "class": "only_routinator"}, len(only_routinator)))
out.append(metric_line("inter_rp_repo_sync_overlap_total", {**base, "class": "neither_available"}, len(neither)))
for diff_class, uris, counts_by_uri in [
("only_ours", only_ours, ours["object_counts"]),
("only_routinator", only_routinator, routinator["object_counts"]),
]:
for rank, uri in enumerate(uris[:50], start=1):
labels = {
**base,
"class": diff_class,
"rank": rank,
"uri": uri,
**object_count_labels(counts_by_uri, uri),
}
if uri in routinator["duration"]:
labels["routinator_duration"] = f"{routinator['duration'][uri]:.3f}"
out.append(metric_line("inter_rp_repo_sync_diff_info", labels, 1))
return {
"oursTotal": len(ours["total"]),
"oursAvailable": len(ours_ok),
"routinatorTotal": len(routinator["total"]),
"routinatorAvailable": len(rout_ok),
"onlyOurs": only_ours[:50],
"onlyRoutinator": only_routinator[:50],
"bothAvailable": len(both),
"neitherAvailable": len(neither),
}
def sync_metrics(now):
path = PEER_ROOT / "sync-status.json"
if not path.exists():
return {"present": False, "success": False, "message": f"missing {path}"}
try:
value = read_json(path)
except Exception as exc:
return {"present": True, "success": False, "message": str(exc)}
ts = parse_rfc3339(value.get("lastSyncAtRfc3339Utc"))
return {
"present": True,
"success": bool(value.get("success")),
"timestamp": ts,
"age": max(0.0, now - ts) if ts is not None else None,
"remote": value.get("remoteHost", ""),
"message": value.get("message", ""),
}
def build_metrics():
now = unix_now()
errors = []
start = time.time()
samples = [sample_ours(now, errors), sample_routinator(now, errors)]
sync = sync_metrics(now)
if not sync.get("success"):
errors.append("sync: " + str(sync.get("message", "failed")))
out = []
out.append(metric_line("inter_rp_service_up", {"instance": INSTANCE}, 1))
out.append(metric_line("inter_rp_service_last_scan_timestamp_seconds", {"instance": INSTANCE}, now))
out.append(metric_line("inter_rp_service_last_scan_duration_seconds", {"instance": INSTANCE}, time.time() - start))
out.append(metric_line("inter_rp_service_last_reload_success", {"instance": INSTANCE}, bool_num(len(errors) == 0)))
out.append(metric_line("inter_rp_parse_errors", {"instance": INSTANCE}, len(errors)))
out.append(metric_line("inter_rp_sync_present", {"instance": INSTANCE}, bool_num(sync.get("present"))))
out.append(metric_line("inter_rp_sync_last_success", {"instance": INSTANCE}, bool_num(sync.get("success"))))
if sync.get("age") is not None:
out.append(metric_line("inter_rp_sync_age_seconds", {"instance": INSTANCE}, sync["age"]))
if sync.get("timestamp") is not None:
out.append(metric_line("inter_rp_sync_last_timestamp_seconds", {"instance": INSTANCE}, sync["timestamp"]))
by_rp = {s["rp"]: s for s in samples}
for s in samples:
labels = {"instance": INSTANCE, "rp": s["rp"]}
out.append(metric_line("inter_rp_run_present", labels, bool_num(s.get("present"))))
out.append(metric_line("inter_rp_run_success", labels, bool_num(s.get("success"))))
out.append(metric_line("inter_rp_sample_parse_errors", labels, s.get("errors", 0)))
if s.get("run_seq") is not None:
out.append(metric_line("inter_rp_run_seq", labels, s["run_seq"]))
if s.get("wall") is not None:
out.append(metric_line("inter_rp_run_wall_seconds", labels, s["wall"]))
if s.get("age") is not None:
out.append(metric_line("inter_rp_artifact_age_seconds", labels, s["age"]))
if s.get("vrps") is not None:
out.append(metric_line("inter_rp_vrps", labels, s["vrps"]))
if s.get("vaps") is not None:
out.append(metric_line("inter_rp_vaps", labels, s["vaps"]))
for kind, value in s.get("max", {}).items():
labels2 = dict(labels)
labels2["kind"] = kind
out.append(metric_line("inter_rp_run_max_rss_bytes", labels2, value))
ours, rout = by_rp.get("ours-rp", {}), by_rp.get("routinator", {})
if ours.get("vrp_set") is not None and rout.get("vrp_set") is not None:
only_ours = ours["vrp_set"] - rout["vrp_set"]
only_rout = rout["vrp_set"] - ours["vrp_set"]
total_diff = len(only_ours) + len(only_rout)
labels = {"instance": INSTANCE, "left": "ours-rp", "right": "routinator"}
out.append(metric_line("inter_rp_vrps_diff", labels, total_diff))
out.append(metric_line("inter_rp_vrps_diff_by_class", {**labels, "class": "total"}, total_diff))
out.append(metric_line("inter_rp_vrps_diff_by_class", {**labels, "class": "only_ours"}, len(only_ours)))
out.append(metric_line("inter_rp_vrps_diff_by_class", {**labels, "class": "only_routinator"}, len(only_rout)))
elif ours.get("vrps") is not None and rout.get("vrps") is not None:
labels = {"instance": INSTANCE, "left": "ours-rp", "right": "routinator"}
total_diff = abs(int(ours["vrps"]) - int(rout["vrps"]))
out.append(metric_line("inter_rp_vrps_diff", labels, total_diff))
out.append(metric_line("inter_rp_vrps_diff_by_class", {**labels, "class": "total"}, total_diff))
if ours.get("vap_set") is not None and rout.get("vap_set") is not None:
only_ours = ours["vap_set"] - rout["vap_set"]
only_rout = rout["vap_set"] - ours["vap_set"]
total_diff = len(only_ours) + len(only_rout)
labels = {"instance": INSTANCE, "left": "ours-rp", "right": "routinator"}
out.append(metric_line("inter_rp_vaps_diff", labels, total_diff))
out.append(metric_line("inter_rp_vaps_diff_by_class", {**labels, "class": "total"}, total_diff))
out.append(metric_line("inter_rp_vaps_diff_by_class", {**labels, "class": "only_ours"}, len(only_ours)))
out.append(metric_line("inter_rp_vaps_diff_by_class", {**labels, "class": "only_routinator"}, len(only_rout)))
elif ours.get("vaps") is not None and rout.get("vaps") is not None:
labels = {"instance": INSTANCE, "left": "ours-rp", "right": "routinator"}
total_diff = abs(int(ours["vaps"]) - int(rout["vaps"]))
out.append(metric_line("inter_rp_vaps_diff", labels, total_diff))
out.append(metric_line("inter_rp_vaps_diff_by_class", {**labels, "class": "total"}, total_diff))
repo_diff = emit_repo_diff_metrics(out, errors)
return "".join(out), {"errors": errors, "samples": samples, "sync": sync, "repoDiff": repo_diff}
def get_metrics():
now = time.time()
with _cache_lock:
if _cache["metrics"] and _cache["deadline"] > now:
return _cache["metrics"]
metrics, status = build_metrics()
_cache["metrics"] = metrics
_cache["status"] = status
_cache["deadline"] = now + SCAN_TTL
return metrics
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/metrics":
body = get_metrics().encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
return
if self.path == "/status":
with _cache_lock:
if not _cache["status"] or _cache["deadline"] <= time.time():
build_metrics()
body = json.dumps(_cache["status"], indent=2).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
return
self.send_response(404)
self.end_headers()
def log_message(self, fmt, *args):
return
def main():
host, port = LISTEN.rsplit(":", 1)
server = ThreadingHTTPServer((host, int(port)), Handler)
print(f"inter-rp ours+routinator exporter listen={LISTEN} instance={INSTANCE}", flush=True)
server.serve_forever()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail
CONFIG_FILE="${INTER_RP_METRICS_CONFIG:-./inter-rp-metrics.env}"
if [[ -f "$CONFIG_FILE" ]]; then
# shellcheck disable=SC1090
source "$CONFIG_FILE"
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INTER_RP_METRICS_EXPORTER="${INTER_RP_METRICS_EXPORTER:-ours-routinator}"
RPKI_INTER_RP_METRICS_BIN="${RPKI_INTER_RP_METRICS_BIN:-./bin/rpki_inter_rp_metrics}"
OURS_ROUTINATOR_EXPORTER="${OURS_ROUTINATOR_EXPORTER:-$SCRIPT_DIR/inter_rp_ours_routinator_exporter.py}"
OURS_RUN_ROOT="${OURS_RUN_ROOT:?OURS_RUN_ROOT is required}"
PEER_ROOT="${PEER_ROOT:-/root/inter-rp-aggregator/synced-from-200}"
LISTEN="${INTER_RP_METRICS_LISTEN:-0.0.0.0:9557}"
POLL_SECS="${INTER_RP_METRICS_POLL_SECS:-30}"
SCAN_TTL_SECONDS="${INTER_RP_SCAN_TTL_SECONDS:-20}"
INSTANCE="${INTER_RP_METRICS_INSTANCE:-remote231-inter-rp}"
LOG_DIR="${INTER_RP_METRICS_LOG_DIR:-./logs}"
mkdir -p "$LOG_DIR"
case "$INTER_RP_METRICS_EXPORTER" in
ours-routinator)
exec env \
OURS_RUN_ROOT="$OURS_RUN_ROOT" \
PEER_ROOT="$PEER_ROOT" \
INTER_RP_INSTANCE="$INSTANCE" \
INTER_RP_LISTEN="$LISTEN" \
INTER_RP_SCAN_TTL_SECONDS="$SCAN_TTL_SECONDS" \
"$OURS_ROUTINATOR_EXPORTER" \
>>"$LOG_DIR/inter-rp-metrics.log" 2>&1
;;
rust-generic)
exec "$RPKI_INTER_RP_METRICS_BIN" \
--ours-run-root "$OURS_RUN_ROOT" \
--peer-root "$PEER_ROOT" \
--listen "$LISTEN" \
--poll-secs "$POLL_SECS" \
--instance "$INSTANCE" \
>>"$LOG_DIR/inter-rp-metrics.log" 2>&1
;;
*)
echo "unknown INTER_RP_METRICS_EXPORTER: $INTER_RP_METRICS_EXPORTER" >&2
exit 2
;;
esac

View File

@@ -0,0 +1,62 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${INTER_RP_CONFIG:-./inter-rp.env}"
if [[ -f "$CONFIG_FILE" ]]; then
# shellcheck disable=SC1090
source "$CONFIG_FILE"
fi
INTER_RP_ROOT="${INTER_RP_ROOT:-/root/inter-rp-runners}"
MAX_RUNS="${MAX_RUNS:--1}"
RUN_INTERVAL_SECS="${RUN_INTERVAL_SECS:-0}"
RETAIN_RUNS="${RETAIN_RUNS:-20}"
RSS_SAMPLE_MS="${RSS_SAMPLE_MS:-500}"
ROUTINATOR_RUN_COMMAND="${ROUTINATOR_RUN_COMMAND:-}"
RPKI_CLIENT_RUN_COMMAND="${RPKI_CLIENT_RUN_COMMAND:-}"
if [[ -z "$ROUTINATOR_RUN_COMMAND" || -z "$RPKI_CLIENT_RUN_COMMAND" ]]; then
cat >&2 <<'MSG'
ROUTINATOR_RUN_COMMAND and RPKI_CLIENT_RUN_COMMAND are required.
Each command runs with RUN_DIR, RP_ROOT, RUN_SEQ, RUN_ID and RP_NAME exported.
MSG
exit 2
fi
mkdir -p "$INTER_RP_ROOT"
run_loop() {
local rp_name="$1"
local run_command="$2"
local rp_root="$INTER_RP_ROOT/$rp_name"
local completed="0"
mkdir -p "$rp_root"
while true; do
if [[ "$MAX_RUNS" =~ ^[0-9]+$ ]] && (( completed >= MAX_RUNS )); then
break
fi
"$SCRIPT_DIR/run_single_rp_with_rss.sh" \
--rp "$rp_name" \
--root "$rp_root" \
--command "$run_command" \
--retain-runs "$RETAIN_RUNS" \
--sample-ms "$RSS_SAMPLE_MS" || true
completed=$((completed + 1))
if (( RUN_INTERVAL_SECS > 0 )); then
sleep "$RUN_INTERVAL_SECS"
fi
done
}
run_loop routinator "$ROUTINATOR_RUN_COMMAND" >"$INTER_RP_ROOT/routinator.loop.log" 2>&1 &
ROUTINATOR_LOOP_PID=$!
run_loop rpki-client "$RPKI_CLIENT_RUN_COMMAND" >"$INTER_RP_ROOT/rpki-client.loop.log" 2>&1 &
RPKI_CLIENT_LOOP_PID=$!
cat >"$INTER_RP_ROOT/runner-pids.env" <<PIDS
ROUTINATOR_LOOP_PID=$ROUTINATOR_LOOP_PID
RPKI_CLIENT_LOOP_PID=$RPKI_CLIENT_LOOP_PID
PIDS
wait "$ROUTINATOR_LOOP_PID" "$RPKI_CLIENT_LOOP_PID"

View File

@@ -0,0 +1,62 @@
#!/usr/bin/env bash
set -euo pipefail
: "${RUN_DIR:?RUN_DIR is required}"
: "${RP_ROOT:?RP_ROOT is required}"
timeout 1800 /root/inter-rp-runners/bin/routinator \
--repository-dir "$RP_ROOT/repository" \
--no-rir-tals \
--extra-tals-dir /var/lib/inter-rp-runners/fixtures/tal \
--enable-aspa \
vrps --format json --output "$RUN_DIR/routinator-output.json"
python3 - "$RUN_DIR/routinator-output.json" "$RUN_DIR/vrps.csv" "$RUN_DIR/vaps.csv" <<'PY'
import csv
import json
import sys
json_path, vrps_path, vaps_path = sys.argv[1:4]
with open(json_path, "r", encoding="utf-8") as handle:
payload = json.load(handle)
def asn_text(value):
if isinstance(value, int):
return f"AS{value}"
text = str(value)
return text if text.startswith("AS") else f"AS{text}"
def asn_sort_key(value):
text = asn_text(value)
try:
return int(text[2:])
except ValueError:
return text
with open(vrps_path, "w", encoding="utf-8", newline="") as handle:
writer = csv.writer(handle)
writer.writerow(["ASN", "IP Prefix", "Max Length", "Trust Anchor"])
for row in payload.get("roas", []):
writer.writerow([
asn_text(row.get("asn", "")),
row.get("prefix", ""),
row.get("maxLength", row.get("max_length", "")),
row.get("ta", row.get("trust_anchor", "")),
])
with open(vaps_path, "w", encoding="utf-8", newline="") as handle:
writer = csv.writer(handle)
writer.writerow(["Customer ASN", "Providers", "Trust Anchor"])
for row in payload.get("aspas", payload.get("aspa", [])):
providers = sorted({asn_text(provider) for provider in row.get("providers", [])}, key=asn_sort_key)
writer.writerow([
asn_text(row.get("customer", row.get("customer_asid", ""))),
";".join(providers),
row.get("ta", row.get("trust_anchor", "unknown")),
])
PY
if [[ "${KEEP_INTER_RP_RAW_JSON:-0}" != "1" ]]; then
rm -f "$RUN_DIR/routinator-output.json"
fi

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -euo pipefail
: "${RUN_DIR:?RUN_DIR is required}"
: "${RP_ROOT:?RP_ROOT is required}"
install -d -o nobody -g nogroup "$RP_ROOT/cache" "$RUN_DIR/output"
chown nobody:nogroup "$RUN_DIR"
timeout 1800 /root/inter-rp-runners/bin/rpki-client-official-9.8 \
-c \
-j \
-t /var/lib/inter-rp-runners/fixtures/tal/afrinic.tal \
-t /var/lib/inter-rp-runners/fixtures/tal/apnic-rfc7730-https.tal \
-t /var/lib/inter-rp-runners/fixtures/tal/arin.tal \
-t /var/lib/inter-rp-runners/fixtures/tal/lacnic.tal \
-t /var/lib/inter-rp-runners/fixtures/tal/ripe-ncc.tal \
-d "$RP_ROOT/cache" \
"$RUN_DIR/output"
cp "$RUN_DIR/output/csv" "$RUN_DIR/vrps.csv"
cp "$RUN_DIR/output/rpki.ccr" "$RUN_DIR/result.ccr"
python3 - "$RUN_DIR/output/json" "$RUN_DIR/vaps.csv" <<'PY'
import csv
import json
import sys
json_path, vaps_path = sys.argv[1:3]
with open(json_path, "r", encoding="utf-8") as handle:
payload = json.load(handle)
def asn_text(value):
if isinstance(value, int):
return f"AS{value}"
text = str(value)
return text if text.startswith("AS") else f"AS{text}"
def asn_sort_key(value):
text = asn_text(value)
try:
return int(text[2:])
except ValueError:
return text
with open(vaps_path, "w", encoding="utf-8", newline="") as handle:
writer = csv.writer(handle)
writer.writerow(["Customer ASN", "Providers", "Trust Anchor"])
for row in payload.get("aspas", []):
providers = sorted({asn_text(provider) for provider in row.get("providers", [])}, key=asn_sort_key)
writer.writerow([
asn_text(row.get("customer_asid", row.get("customer", ""))),
";".join(providers),
row.get("ta", row.get("trust_anchor", "unknown")),
])
PY
rm -rf "$RUN_DIR/output"

View File

@@ -0,0 +1,266 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Usage: run_single_rp_with_rss.sh --rp <name> --root <path> --command <shell-command> [--retain-runs <n>] [--sample-ms <n>]
The command runs with RUN_DIR, RP_ROOT, RUN_SEQ, RUN_ID, and RP_NAME exported.
It must write artifacts into RUN_DIR. The wrapper writes run-meta.json and atomically updates latest.
USAGE
}
RP_NAME=""
RP_ROOT=""
RUN_COMMAND=""
RETAIN_RUNS="${RETAIN_RUNS:-20}"
SAMPLE_MS="${RSS_SAMPLE_MS:-500}"
while [[ $# -gt 0 ]]; do
case "$1" in
--rp)
RP_NAME="$2"
shift 2
;;
--root)
RP_ROOT="$2"
shift 2
;;
--command)
RUN_COMMAND="$2"
shift 2
;;
--retain-runs)
RETAIN_RUNS="$2"
shift 2
;;
--sample-ms)
SAMPLE_MS="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done
if [[ -z "$RP_NAME" || -z "$RP_ROOT" || -z "$RUN_COMMAND" ]]; then
usage >&2
exit 2
fi
mkdir -p "$RP_ROOT/runs" "$RP_ROOT/logs"
next_seq() {
local max_seq="0"
local run_path
shopt -s nullglob
for run_path in "$RP_ROOT"/runs/run_*; do
local run_name="${run_path##*/}"
local seq="${run_name#run_}"
if [[ "$seq" =~ ^[0-9]+$ ]] && (( 10#$seq > max_seq )); then
max_seq=$((10#$seq))
fi
done
shopt -u nullglob
printf '%06d' $((max_seq + 1))
}
rss_kb_for_pid() {
local pid="$1"
awk '/^VmRSS:/ {print $2; found=1} END {if (!found) print 0}' "/proc/$pid/status" 2>/dev/null || echo 0
}
collect_related_pids() {
local root_pid="$1"
local process_group="$2"
local queue=("$root_pid")
local pid
declare -A seen=()
if [[ -n "$process_group" ]]; then
while read -r pid; do
[[ -n "$pid" ]] && queue+=("$pid")
done < <(pgrep -g "$process_group" 2>/dev/null || true)
fi
while ((${#queue[@]} > 0)); do
pid="${queue[0]}"
queue=("${queue[@]:1}")
[[ -z "$pid" || -n "${seen[$pid]:-}" ]] && continue
seen[$pid]=1
echo "$pid"
while read -r child_pid; do
[[ -n "$child_pid" ]] && queue+=("$child_pid")
done < <(pgrep -P "$pid" 2>/dev/null || true)
done
}
sum_related_rss() {
local process_group="$1"
local parent_pid="$2"
local total_rss="0"
local child_max_rss="0"
local pid
while read -r pid; do
[[ -z "$pid" ]] && continue
local rss
rss="$(rss_kb_for_pid "$pid")"
total_rss=$((total_rss + rss))
if [[ "$pid" != "$parent_pid" ]] && (( rss > child_max_rss )); then
child_max_rss="$rss"
fi
done < <(collect_related_pids "$parent_pid" "$process_group")
printf '%s %s
' "$total_rss" "$child_max_rss"
}
count_csv_rows() {
local path="$1"
if [[ ! -f "$path" ]]; then
echo 0
return
fi
awk 'BEGIN {count=0} /^[[:space:]]*$/ {next} /^#/ {next} NR==1 {next} {count++} END {print count}' "$path"
}
RUN_SEQ="$(next_seq)"
RUN_ID="run_${RUN_SEQ}"
RUN_DIR="$RP_ROOT/runs/$RUN_ID"
mkdir -p "$RUN_DIR"
STARTED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
START_EPOCH_MS="$(python3 - <<'PY'
import time
print(int(time.time() * 1000))
PY
)"
export RP_NAME RP_ROOT RUN_SEQ RUN_ID RUN_DIR RUN_COMMAND
set +e
setsid bash -lc "$RUN_COMMAND" >"$RUN_DIR/stdout.log" 2>"$RUN_DIR/stderr.log" &
CHILD_PID=$!
set -e
sleep 0.05
PROCESS_GROUP="$(ps -o pgid= -p "$CHILD_PID" 2>/dev/null | tr -d '[:space:]' || true)"
if [[ -z "$PROCESS_GROUP" ]]; then
PROCESS_GROUP="$CHILD_PID"
fi
PARENT_MAX_RSS_KB="0"
CHILD_MAX_RSS_KB="0"
AGGREGATE_PEAK_RSS_KB="0"
SAMPLE_INTERVAL_SECONDS="$(python3 - <<PY
print(max(float($SAMPLE_MS) / 1000.0, 0.05))
PY
)"
while kill -0 "$CHILD_PID" 2>/dev/null; do
parent_rss="$(rss_kb_for_pid "$CHILD_PID")"
read -r aggregate_rss child_rss < <(sum_related_rss "$PROCESS_GROUP" "$CHILD_PID")
if (( parent_rss > PARENT_MAX_RSS_KB )); then
PARENT_MAX_RSS_KB="$parent_rss"
fi
if (( child_rss > CHILD_MAX_RSS_KB )); then
CHILD_MAX_RSS_KB="$child_rss"
fi
if (( aggregate_rss > AGGREGATE_PEAK_RSS_KB )); then
AGGREGATE_PEAK_RSS_KB="$aggregate_rss"
fi
sleep "$SAMPLE_INTERVAL_SECONDS"
done
set +e
wait "$CHILD_PID"
EXIT_CODE=$?
set -e
FINISHED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
END_EPOCH_MS="$(python3 - <<'PY'
import time
print(int(time.time() * 1000))
PY
)"
WALL_MS=$((END_EPOCH_MS - START_EPOCH_MS))
SUCCESS=false
if [[ "$EXIT_CODE" == "0" ]]; then
SUCCESS=true
fi
VRPS_COUNT="$(count_csv_rows "$RUN_DIR/vrps.csv")"
VAPS_COUNT="$(count_csv_rows "$RUN_DIR/vaps.csv")"
export STARTED_AT FINISHED_AT WALL_MS EXIT_CODE SUCCESS
export PARENT_MAX_RSS_KB CHILD_MAX_RSS_KB AGGREGATE_PEAK_RSS_KB SAMPLE_MS
export VRPS_COUNT VAPS_COUNT
export CCR_ARTIFACT_PATH=""
export VRPS_ARTIFACT_PATH=""
export VAPS_ARTIFACT_PATH=""
if [[ -f "$RUN_DIR/result.ccr" ]]; then
CCR_ARTIFACT_PATH="result.ccr"
fi
if [[ -f "$RUN_DIR/vrps.csv" ]]; then
VRPS_ARTIFACT_PATH="vrps.csv"
fi
if [[ -f "$RUN_DIR/vaps.csv" ]]; then
VAPS_ARTIFACT_PATH="vaps.csv"
fi
python3 - <<'PY' >"$RUN_DIR/run-meta.json"
import os
import json, socket
def optional(name):
value = os.environ.get(name, "")
return value if value else None
meta = {
"schemaVersion": 1,
"rp": os.environ["RP_NAME"],
"runSeq": int(os.environ["RUN_SEQ"]),
"runId": os.environ["RUN_ID"],
"host": socket.gethostname(),
"command": os.environ["RUN_COMMAND"],
"startedAtRfc3339Utc": os.environ["STARTED_AT"],
"finishedAtRfc3339Utc": os.environ["FINISHED_AT"],
"wallMs": int(os.environ["WALL_MS"]),
"exitCode": int(os.environ["EXIT_CODE"]),
"success": os.environ["SUCCESS"] == "true",
"maxRssKb": {
"parent": int(os.environ["PARENT_MAX_RSS_KB"]),
"childMax": int(os.environ["CHILD_MAX_RSS_KB"]),
"aggregatePeak": int(os.environ["AGGREGATE_PEAK_RSS_KB"]),
"sampleIntervalMs": int(os.environ["SAMPLE_MS"]),
},
"artifacts": {
"vrpsCsv": optional("VRPS_ARTIFACT_PATH"),
"vapsCsv": optional("VAPS_ARTIFACT_PATH"),
"ccr": optional("CCR_ARTIFACT_PATH"),
"stdout": "stdout.log",
"stderr": "stderr.log",
},
"counts": {
"vrps": int(os.environ["VRPS_COUNT"]),
"vaps": int(os.environ["VAPS_COUNT"]),
},
}
print(json.dumps(meta, indent=2, ensure_ascii=False))
PY
ln -sfn "runs/$RUN_ID" "$RP_ROOT/latest.tmp"
mv -Tf "$RP_ROOT/latest.tmp" "$RP_ROOT/latest"
echo "$RUN_ID $RP_NAME exit=$EXIT_CODE wall_ms=$WALL_MS vrps=$VRPS_COUNT vaps=$VAPS_COUNT rss_kb=$AGGREGATE_PEAK_RSS_KB"
if [[ "$RETAIN_RUNS" =~ ^[0-9]+$ ]] && (( RETAIN_RUNS > 0 )); then
mapfile -t old_runs < <(find "$RP_ROOT/runs" -maxdepth 1 -type d -name 'run_*' -printf '%f\n' | sort | head -n -"$RETAIN_RUNS" || true)
for old_run in "${old_runs[@]}"; do
rm -rf "$RP_ROOT/runs/$old_run"
done
fi
exit "$EXIT_CODE"

View File

@@ -0,0 +1,90 @@
#!/usr/bin/env bash
set -euo pipefail
CONFIG_FILE="${INTER_RP_SYNC_CONFIG:-./inter-rp-sync.env}"
if [[ -f "$CONFIG_FILE" ]]; then
# shellcheck disable=SC1090
source "$CONFIG_FILE"
fi
REMOTE200="${REMOTE200:-root@43.110.128.200}"
REMOTE200_ROOT="${REMOTE200_ROOT:-/root/inter-rp-runners}"
PEER_ROOT="${PEER_ROOT:-/root/inter-rp-aggregator/synced-from-200}"
SYNC_INTERVAL_SECS="${SYNC_INTERVAL_SECS:-60}"
MAX_SYNCS="${MAX_SYNCS:--1}"
RPS="${RPS:-routinator rpki-client}"
mkdir -p "$PEER_ROOT"
write_status() {
local success="$1"
local message="$2"
export SYNC_SUCCESS="$success"
export SYNC_MESSAGE="$message"
export SYNC_REMOTE200="$REMOTE200"
export SYNC_REMOTE200_ROOT="$REMOTE200_ROOT"
python3 - <<'PY' >"$PEER_ROOT/sync-status.json"
import json, socket, datetime
print(json.dumps({
"schemaVersion": 1,
"success": __import__("os").environ["SYNC_SUCCESS"] == "true",
"lastSyncAtRfc3339Utc": datetime.datetime.now(datetime.UTC).replace(microsecond=0).isoformat().replace('+00:00', 'Z'),
"remoteHost": __import__("os").environ["SYNC_REMOTE200"],
"remoteRoot": __import__("os").environ["SYNC_REMOTE200_ROOT"],
"localHost": socket.gethostname(),
"message": __import__("os").environ["SYNC_MESSAGE"],
}, indent=2))
PY
}
sync_once() {
local rp_name
local temp_root="$PEER_ROOT/.sync-tmp-$$"
rm -rf "$temp_root"
mkdir -p "$temp_root"
for rp_name in $RPS; do
mkdir -p "$temp_root/$rp_name"
if ! rsync -aL --delete \
--include='run-meta.json' \
--include='result.ccr' \
--include='vrps.csv' \
--include='vaps.csv' \
--include='stdout.log' \
--include='stderr.log' \
--exclude='*' \
"$REMOTE200:$REMOTE200_ROOT/$rp_name/latest/" "$temp_root/$rp_name/latest/"; then
echo "sync failed for $rp_name; clearing stale local latest" >&2
rm -rf "$PEER_ROOT/$rp_name/latest" "$PEER_ROOT/$rp_name/latest.next" "$PEER_ROOT/$rp_name/latest.prev"
return 1
fi
if [[ ! -d "$temp_root/$rp_name/latest" ]]; then
echo "missing synced latest directory for $rp_name" >&2
rm -rf "$PEER_ROOT/$rp_name/latest" "$PEER_ROOT/$rp_name/latest.next" "$PEER_ROOT/$rp_name/latest.prev"
return 1
fi
rm -rf "$PEER_ROOT/$rp_name/latest.next" "$PEER_ROOT/$rp_name/latest.prev"
mkdir -p "$PEER_ROOT/$rp_name"
mv "$temp_root/$rp_name/latest" "$PEER_ROOT/$rp_name/latest.next"
if [[ -e "$PEER_ROOT/$rp_name/latest" ]]; then
mv "$PEER_ROOT/$rp_name/latest" "$PEER_ROOT/$rp_name/latest.prev"
fi
mv "$PEER_ROOT/$rp_name/latest.next" "$PEER_ROOT/$rp_name/latest"
rm -rf "$PEER_ROOT/$rp_name/latest.prev"
done
rm -rf "$temp_root"
}
completed="0"
while true; do
if sync_once; then
write_status true "ok"
else
rm -rf "$PEER_ROOT/.sync-tmp-$$"
write_status false "rsync failed"
fi
completed=$((completed + 1))
if [[ "$MAX_SYNCS" =~ ^[0-9]+$ ]] && (( completed >= MAX_SYNCS )); then
break
fi
sleep "$SYNC_INTERVAL_SECS"
done

View File

@@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -euo pipefail
REMOTE231="${REMOTE231:-root@47.251.127.231}"
REMOTE_ROOT="${REMOTE_ROOT:-/var/lib/inter-rp-runners}"
PEER_ROOT="${PEER_ROOT:-/root/ours-rp-continuous/portable-soak/inter-rp-peers}"
SYNC_INTERVAL_SECS="${SYNC_INTERVAL_SECS:-60}"
MAX_SYNCS="${MAX_SYNCS:--1}"
LOG_PREFIX="[inter-rp-sync]"
mkdir -p "$PEER_ROOT/routinator"
write_status() {
local success="$1"
local message="$2"
env SYNC_SUCCESS="$success" SYNC_MESSAGE="$message" SYNC_REMOTE="$REMOTE231" SYNC_REMOTE_ROOT="$REMOTE_ROOT" python3 - "$PEER_ROOT/sync-status.json" <<'PY'
import datetime, json, os, socket, sys
path = sys.argv[1]
payload = {
"schemaVersion": 1,
"success": os.environ["SYNC_SUCCESS"] == "true",
"lastSyncAtRfc3339Utc": datetime.datetime.now(datetime.UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
"remoteHost": os.environ["SYNC_REMOTE"],
"remoteRoot": os.environ["SYNC_REMOTE_ROOT"],
"localHost": socket.gethostname(),
"message": os.environ["SYNC_MESSAGE"],
}
with open(path, "w", encoding="utf-8") as handle:
json.dump(payload, handle, indent=2)
handle.write("\n")
PY
}
sync_once() {
local tmp="$PEER_ROOT/.sync-routinator-$$"
rm -rf "$tmp"
mkdir -p "$tmp"
if ! rsync -aL --delete \
--include='run-meta.json' \
--include='result.ccr' \
--include='vrps.csv' \
--include='vaps.csv' \
--include='stdout.log' \
--include='stderr.log' \
--exclude='*' \
"$REMOTE231:$REMOTE_ROOT/routinator/latest/" "$tmp/latest/"; then
rm -rf "$tmp"
return 1
fi
if [[ ! -f "$tmp/latest/run-meta.json" ]]; then
rm -rf "$tmp"
return 2
fi
if ! ssh -o BatchMode=yes -o ConnectTimeout=8 "$REMOTE231" "curl -fsS --max-time 20 http://127.0.0.1:9558/metrics" >"$tmp/routinator-metrics.prom"; then
rm -rf "$tmp"
return 3
fi
rm -rf "$PEER_ROOT/routinator/latest.next" "$PEER_ROOT/routinator/latest.prev" "$PEER_ROOT/routinator/routinator-metrics.prom.next"
mv "$tmp/routinator-metrics.prom" "$PEER_ROOT/routinator/routinator-metrics.prom.next"
mv "$PEER_ROOT/routinator/routinator-metrics.prom.next" "$PEER_ROOT/routinator/routinator-metrics.prom"
mv "$tmp/latest" "$PEER_ROOT/routinator/latest.next"
if [[ -e "$PEER_ROOT/routinator/latest" ]]; then
mv "$PEER_ROOT/routinator/latest" "$PEER_ROOT/routinator/latest.prev"
fi
mv "$PEER_ROOT/routinator/latest.next" "$PEER_ROOT/routinator/latest"
rm -rf "$PEER_ROOT/routinator/latest.prev" "$tmp"
}
completed=0
while true; do
if sync_once; then
echo "$LOG_PREFIX $(date -u +%Y-%m-%dT%H:%M:%SZ) ok"
write_status true "ok"
else
code=$?
echo "$LOG_PREFIX $(date -u +%Y-%m-%dT%H:%M:%SZ) sync failed code=$code" >&2
write_status false "routinator rsync failed code=$code"
fi
completed=$((completed + 1))
if [[ "$MAX_SYNCS" =~ ^[0-9]+$ ]] && (( completed >= MAX_SYNCS )); then
break
fi
sleep "$SYNC_INTERVAL_SECS"
done

View File

@@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
build_local_repo_replay_package.sh --out <path> [--tar-gz]
Build a standalone local repository tree replay package for Routinator and
rpki-client. The package does not include repository data and does not include
materialize tooling.
EOF
}
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
SRC_DIR="$ROOT_DIR/scripts/local_repo_replay"
OUT=""
TAR_GZ=0
while [[ $# -gt 0 ]]; do
case "$1" in
--out) OUT="$2"; shift 2 ;;
--tar-gz) TAR_GZ=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
done
[[ -n "$OUT" ]] || { usage >&2; exit 2; }
if [[ "$TAR_GZ" -eq 1 ]]; then
PACKAGE_DIR="$(mktemp -d)"
TARGET_DIR="$PACKAGE_DIR/local-repo-replay-package"
else
TARGET_DIR="$OUT"
rm -rf "$TARGET_DIR"
fi
mkdir -p "$TARGET_DIR/scripts" "$TARGET_DIR/docs" "$TARGET_DIR/examples"
install -m 0755 "$SRC_DIR/run_routinator_from_local_tree.sh" "$TARGET_DIR/scripts/"
install -m 0755 "$SRC_DIR/run_rpki_client_from_local_tree.sh" "$TARGET_DIR/scripts/"
install -m 0755 "$SRC_DIR/run_dual_local_tree_replay.sh" "$TARGET_DIR/scripts/"
install -m 0755 "$SRC_DIR/prepare_tals.py" "$TARGET_DIR/scripts/"
install -m 0755 "$SRC_DIR/normalize_rp_outputs.py" "$TARGET_DIR/scripts/"
install -m 0755 "$SRC_DIR/compare_normalized_sets.py" "$TARGET_DIR/scripts/"
install -m 0755 "$SRC_DIR/summarize_replay.py" "$TARGET_DIR/scripts/"
install -m 0755 "$ROOT_DIR/scripts/cir/cir-rsync-wrapper" "$TARGET_DIR/scripts/"
install -m 0755 "$ROOT_DIR/scripts/cir/cir-local-link-sync.py" "$TARGET_DIR/scripts/"
install -m 0644 "$SRC_DIR/templates/README.md" "$TARGET_DIR/README.md"
install -m 0644 "$SRC_DIR/templates/docs/input_tree_requirements.md" "$TARGET_DIR/docs/"
install -m 0644 "$SRC_DIR/templates/docs/offline_replay_limits.md" "$TARGET_DIR/docs/"
install -m 0644 "$SRC_DIR/templates/docs/output_files.md" "$TARGET_DIR/docs/"
install -m 0755 "$SRC_DIR/templates/examples/routinator_example.sh" "$TARGET_DIR/examples/"
install -m 0755 "$SRC_DIR/templates/examples/rpki_client_example.sh" "$TARGET_DIR/examples/"
install -m 0755 "$SRC_DIR/templates/examples/dual_compare_example.sh" "$TARGET_DIR/examples/"
cat > "$TARGET_DIR/env.example" <<'EOF'
# 本地目录树 replay 示例配置。目录树由使用者提前准备,不包含在 package 中。
TAL_DIR=/data/replay/tals
MIRROR_ROOT=/data/replay/mirror
ROUTINATOR_BIN=/opt/routinator/target/release/routinator
RPKI_CLIENT_BIN=/opt/rpki-client/src/rpki-client
RPKI_CLIENT_CACHE_DIR=/data/replay/work/rpki-client-cache
VALIDATION_TIME=2026-05-23T00:00:00Z
EOF
if grep -R -E 'cir_materialize|repo-bytes\\.db|\\.cir' "$TARGET_DIR/scripts" >/dev/null; then
echo "package contains forbidden materialize/repo-bytes implementation references" >&2
exit 1
fi
if [[ "$TAR_GZ" -eq 1 ]]; then
mkdir -p "$(dirname "$OUT")"
tar -C "$PACKAGE_DIR" -czf "$OUT" local-repo-replay-package
rm -rf "$PACKAGE_DIR"
echo "$OUT"
else
echo "$TARGET_DIR"
fi

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from pathlib import Path
def read_set(path: Path) -> set[str]:
if not path.is_file():
return set()
return {line.strip() for line in path.read_text(encoding="utf-8", errors="replace").splitlines() if line.strip()}
def compare(left: set[str], right: set[str]) -> dict[str, object]:
union = left | right
inter = left & right
return {
"left": len(left),
"right": len(right),
"intersection": len(inter),
"onlyLeft": len(left - right),
"onlyRight": len(right - left),
"jaccard": round(len(inter) / len(union), 8) if union else 1.0,
"onlyLeftSamples": sorted(left - right)[:20],
"onlyRightSamples": sorted(right - left)[:20],
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--left", type=Path, required=True)
parser.add_argument("--right", type=Path, required=True)
parser.add_argument("--left-name", default="left")
parser.add_argument("--right-name", default="right")
parser.add_argument("--out", type=Path, required=True)
args = parser.parse_args()
result = {
"leftName": args.left_name,
"rightName": args.right_name,
"vrps": compare(read_set(args.left / "vrps.normalized.txt"), read_set(args.right / "vrps.normalized.txt")),
"vaps": compare(read_set(args.left / "vaps.normalized.txt"), read_set(args.right / "vaps.normalized.txt")),
}
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(args.out)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,123 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import csv
import json
from pathlib import Path
from typing import Any
def normalize_asn(value: Any) -> str:
text = str(value).strip().upper()
if text.startswith("AS"):
text = text[2:]
return f"AS{int(text)}"
def write_set(path: Path, rows: set[str]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("\n".join(sorted(rows)) + ("\n" if rows else ""), encoding="utf-8")
def load_json(path: Path) -> Any:
return json.loads(path.read_text(encoding="utf-8"))
def normalize_vrps_from_json(path: Path) -> set[str]:
data = load_json(path)
rows: set[str] = set()
objects: list[dict[str, Any]] = []
if isinstance(data, dict):
for key in ("roas", "routeOrigins", "valid_roas"):
value = data.get(key)
if isinstance(value, list):
objects.extend(item for item in value if isinstance(item, dict))
elif isinstance(data, list):
objects = [item for item in data if isinstance(item, dict)]
for item in objects:
asn = item.get("asn") or item.get("asID") or item.get("origin")
prefix = item.get("prefix")
max_length = item.get("maxLength") or item.get("max_length") or item.get("maxlen") or item.get("maxLengthPrefix")
if asn is None or prefix is None or max_length is None:
continue
rows.add(f"{normalize_asn(asn)}|{prefix}|{int(max_length)}")
return rows
def normalize_vaps_from_json(path: Path) -> set[str]:
data = load_json(path)
rows: set[str] = set()
objects: list[dict[str, Any]] = []
if isinstance(data, dict):
for key in ("aspas", "aspaAssertions", "vaps"):
value = data.get(key)
if isinstance(value, list):
objects.extend(item for item in value if isinstance(item, dict))
elif isinstance(data, list):
objects = [item for item in data if isinstance(item, dict)]
for item in objects:
customer = (
item.get("customer")
or item.get("customer_asid")
or item.get("customerASID")
or item.get("customerAsid")
or item.get("customerAsn")
)
providers = item.get("providers") or item.get("provider_asns") or item.get("providerASNs") or []
if customer is None:
continue
provider_asns = [normalize_asn(provider) for provider in providers]
rows.add(f"{normalize_asn(customer)}|{','.join(sorted(set(provider_asns), key=lambda value: int(value[2:])))}")
return rows
def normalize_vrps_from_csv(path: Path) -> set[str]:
rows: set[str] = set()
with path.open(newline="", encoding="utf-8", errors="replace") as handle:
for row in csv.DictReader(handle):
asn = row.get("ASN") or row.get("asn") or row.get("AS")
prefix = row.get("IP Prefix") or row.get("prefix") or row.get("Prefix")
max_length = row.get("Max Length") or row.get("maxLength") or row.get("max_length")
if asn and prefix and max_length:
rows.add(f"{normalize_asn(asn)}|{prefix}|{int(max_length)}")
return rows
def normalize_routinator(input_path: Path) -> tuple[set[str], set[str]]:
return normalize_vrps_from_json(input_path), normalize_vaps_from_json(input_path)
def normalize_rpki_client(input_path: Path) -> tuple[set[str], set[str]]:
json_path = input_path / "json" if input_path.is_dir() else input_path
csv_path = input_path / "csv" if input_path.is_dir() else input_path
vrps: set[str] = set()
vaps: set[str] = set()
if json_path.is_file():
vrps |= normalize_vrps_from_json(json_path)
vaps |= normalize_vaps_from_json(json_path)
if csv_path.is_file():
vrps |= normalize_vrps_from_csv(csv_path)
return vrps, vaps
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--kind", choices=["routinator", "rpki-client"], required=True)
parser.add_argument("--input", type=Path, required=True)
parser.add_argument("--vrps-out", type=Path, required=True)
parser.add_argument("--vaps-out", type=Path, required=True)
args = parser.parse_args()
if args.kind == "routinator":
vrps, vaps = normalize_routinator(args.input)
else:
vrps, vaps = normalize_rpki_client(args.input)
write_set(args.vrps_out, vrps)
write_set(args.vaps_out, vaps)
print(json.dumps({"vrps": len(vrps), "vaps": len(vaps)}, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,84 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from pathlib import Path
def split_tal(text: str) -> tuple[list[str], list[str]]:
lines = text.splitlines()
for index, line in enumerate(lines):
if line.strip() == "":
return lines[:index], lines[index + 1 :]
return lines, []
def rsync_only_tal(source: Path) -> tuple[str, dict[str, object]]:
text = source.read_text(encoding="utf-8", errors="replace")
uri_lines, key_lines = split_tal(text)
uris = [
line.strip()
for line in uri_lines
if line.strip() and not line.lstrip().startswith("#")
]
rsync_uris = [
uri for uri in uris
if uri.lower().startswith("rsync://")
]
if not rsync_uris:
return text if text.endswith("\n") else text + "\n", {
"file": source.name,
"mode": "unchanged_no_rsync_uri",
"uris": len(uris),
"rsyncUris": 0,
}
out = "\n".join(rsync_uris) + "\n\n" + "\n".join(key_lines).strip() + "\n"
return out, {
"file": source.name,
"mode": "rsync_only",
"uris": len(uris),
"rsyncUris": len(rsync_uris),
}
def collect_tals(tal_dir: Path | None, tals: list[Path]) -> list[Path]:
paths: list[Path] = []
if tal_dir is not None:
paths.extend(sorted(tal_dir.glob("*.tal")))
paths.extend(tals)
unique: dict[str, Path] = {}
for path in paths:
unique[path.name] = path
return [unique[name] for name in sorted(unique)]
def main() -> int:
parser = argparse.ArgumentParser(description="Prepare TALs for local rsync-tree replay.")
parser.add_argument("--tal-dir", type=Path)
parser.add_argument("--tal", type=Path, action="append", default=[])
parser.add_argument("--out-dir", type=Path, required=True)
parser.add_argument("--summary", type=Path)
args = parser.parse_args()
sources = collect_tals(args.tal_dir, args.tal)
if not sources:
raise SystemExit("no TAL files provided")
args.out_dir.mkdir(parents=True, exist_ok=True)
rows = []
for source in sources:
if not source.is_file():
raise SystemExit(f"TAL file not found: {source}")
content, row = rsync_only_tal(source)
(args.out_dir / source.name).write_text(content, encoding="utf-8")
rows.append(row)
if args.summary is not None:
args.summary.parent.mkdir(parents=True, exist_ok=True)
args.summary.write_text(json.dumps(rows, indent=2, sort_keys=True) + "\n", encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
run_dual_local_tree_replay.sh \
--routinator-bin <path> --routinator-mirror-root <dir> \
--rpki-client-bin <path> --rpki-client-mirror-root <dir> \
--tal-dir <dir> --out-dir <dir> [--validation-time <RFC3339>]
EOF
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROUTINATOR_BIN=""
ROUTINATOR_MIRROR_ROOT=""
RPKI_CLIENT_BIN=""
RPKI_CLIENT_MIRROR_ROOT=""
RPKI_CLIENT_CACHE_DIR=""
TAL_DIR=""
OUT_DIR=""
VALIDATION_TIME=""
while [[ $# -gt 0 ]]; do
case "$1" in
--routinator-bin) ROUTINATOR_BIN="$2"; shift 2 ;;
--routinator-mirror-root) ROUTINATOR_MIRROR_ROOT="$2"; shift 2 ;;
--rpki-client-bin) RPKI_CLIENT_BIN="$2"; shift 2 ;;
--rpki-client-mirror-root) RPKI_CLIENT_MIRROR_ROOT="$2"; shift 2 ;;
--rpki-client-cache-dir) RPKI_CLIENT_CACHE_DIR="$2"; shift 2 ;;
--tal-dir) TAL_DIR="$2"; shift 2 ;;
--out-dir) OUT_DIR="$2"; shift 2 ;;
--validation-time) VALIDATION_TIME="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
done
if [[ -z "$RPKI_CLIENT_MIRROR_ROOT" ]]; then
RPKI_CLIENT_MIRROR_ROOT="$ROUTINATOR_MIRROR_ROOT"
fi
[[ -n "$ROUTINATOR_BIN" && -n "$ROUTINATOR_MIRROR_ROOT" && -n "$RPKI_CLIENT_BIN" && -n "$RPKI_CLIENT_MIRROR_ROOT" && -n "$TAL_DIR" && -n "$OUT_DIR" ]] || { usage >&2; exit 2; }
mkdir -p "$OUT_DIR"
TIME_ARGS=()
if [[ -n "$VALIDATION_TIME" ]]; then
TIME_ARGS=(--validation-time "$VALIDATION_TIME")
fi
CACHE_ARGS=()
if [[ -n "$RPKI_CLIENT_CACHE_DIR" ]]; then
CACHE_ARGS=(--cache-dir "$RPKI_CLIENT_CACHE_DIR")
fi
"$SCRIPT_DIR/run_routinator_from_local_tree.sh" \
--routinator-bin "$ROUTINATOR_BIN" \
--mirror-root "$ROUTINATOR_MIRROR_ROOT" \
--tal-dir "$TAL_DIR" \
--out-dir "$OUT_DIR/routinator" \
--enable-aspa \
"${TIME_ARGS[@]}"
"$SCRIPT_DIR/run_rpki_client_from_local_tree.sh" \
--rpki-client-bin "$RPKI_CLIENT_BIN" \
--mirror-root "$RPKI_CLIENT_MIRROR_ROOT" \
--tal-dir "$TAL_DIR" \
--out-dir "$OUT_DIR/rpki-client" \
"${CACHE_ARGS[@]}" \
"${TIME_ARGS[@]}"
python3 "$SCRIPT_DIR/compare_normalized_sets.py" \
--left "$OUT_DIR/routinator" \
--right "$OUT_DIR/rpki-client" \
--left-name routinator \
--right-name rpki-client \
--out "$OUT_DIR/compare-summary.json"
echo "done: $OUT_DIR"

View File

@@ -0,0 +1,131 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
run_routinator_from_local_tree.sh \
--routinator-bin <path> \
--mirror-root <local-rsync-mirror-root> \
--tal-dir <dir> | --tal <file>... \
--out-dir <path> \
[--validation-time <RFC3339>] \
[--real-rsync-bin <path>] \
[--enable-aspa]
The input mirror is prepared by the caller. This script does not generate it.
Pass --validation-time only if FAKETIME_LIB points to a working libfaketime
library; otherwise Routinator will validate at wall-clock time.
Example:
export FAKETIME_LIB=/usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1
EOF
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROUTINATOR_BIN=""
MIRROR_ROOT=""
OUT_DIR=""
VALIDATION_TIME=""
REAL_RSYNC_BIN="${REAL_RSYNC_BIN:-/usr/bin/rsync}"
ENABLE_ASPA=0
TAL_DIR=""
TALS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--routinator-bin) ROUTINATOR_BIN="$2"; shift 2 ;;
--mirror-root) MIRROR_ROOT="$2"; shift 2 ;;
--tal-dir) TAL_DIR="$2"; shift 2 ;;
--tal) TALS+=("$2"); shift 2 ;;
--out-dir) OUT_DIR="$2"; shift 2 ;;
--validation-time) VALIDATION_TIME="$2"; shift 2 ;;
--real-rsync-bin) REAL_RSYNC_BIN="$2"; shift 2 ;;
--enable-aspa) ENABLE_ASPA=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
done
[[ -n "$ROUTINATOR_BIN" && -n "$MIRROR_ROOT" && -n "$OUT_DIR" ]] || { usage >&2; exit 2; }
[[ -x "$ROUTINATOR_BIN" ]] || { echo "routinator binary not executable: $ROUTINATOR_BIN" >&2; exit 2; }
[[ -d "$MIRROR_ROOT" ]] || { echo "mirror root not found: $MIRROR_ROOT" >&2; exit 2; }
if [[ -z "$TAL_DIR" && "${#TALS[@]}" -eq 0 ]]; then
echo "provide --tal-dir or at least one --tal" >&2
exit 2
fi
mkdir -p "$OUT_DIR"
WORK_DIR="$OUT_DIR/work"
REPO_DIR="$WORK_DIR/repository"
EXTRA_TALS="$WORK_DIR/tals"
CONFIG_FILE="$WORK_DIR/routinator.conf"
rm -rf "$WORK_DIR"
mkdir -p "$REPO_DIR" "$EXTRA_TALS"
PREPARE_TAL_ARGS=(--out-dir "$EXTRA_TALS" --summary "$OUT_DIR/prepared-tals.json")
if [[ -n "$TAL_DIR" ]]; then
PREPARE_TAL_ARGS+=(--tal-dir "$TAL_DIR")
fi
for tal in "${TALS[@]}"; do
PREPARE_TAL_ARGS+=(--tal "$tal")
done
python3 "$SCRIPT_DIR/prepare_tals.py" "${PREPARE_TAL_ARGS[@]}"
cat > "$CONFIG_FILE" <<EOF
repository-dir = "$REPO_DIR"
no-rir-tals = true
extra-tals-dir = "$EXTRA_TALS"
disable-rrdp = true
rrdp-fallback = "never"
rsync-command = "$SCRIPT_DIR/cir-rsync-wrapper"
EOF
export CIR_MIRROR_ROOT="$(cd "$MIRROR_ROOT" && pwd)"
export REAL_RSYNC_BIN="$REAL_RSYNC_BIN"
export CIR_LOCAL_LINK_MODE=1
export LOCAL_REPO_REPLAY_VALIDATION_TIME="$VALIDATION_TIME"
export LOCAL_REPO_REPLAY_OUT_DIR="$OUT_DIR"
ARGS=(
"$ROUTINATOR_BIN"
--config "$CONFIG_FILE"
--repository-dir "$REPO_DIR"
--disable-rrdp
--rrdp-fallback never
--rsync-command "$SCRIPT_DIR/cir-rsync-wrapper"
--no-rir-tals
--extra-tals-dir "$EXTRA_TALS"
)
if [[ "$ENABLE_ASPA" -eq 1 ]]; then
echo 'enable-aspa = true' >> "$CONFIG_FILE"
ARGS+=(--enable-aspa)
fi
if [[ -n "$VALIDATION_TIME" && -z "${FAKETIME_LIB:-}" ]]; then
echo "warning: --validation-time is ignored for Routinator because FAKETIME_LIB is not set" >&2
fi
VRP_ARGS=(vrps -o "$OUT_DIR/vrps.csv")
JSON_ARGS=(vrps -f jsonext -o "$OUT_DIR/routinator.json")
/usr/bin/time -v -o "$OUT_DIR/process-time.txt" bash -c '
set -euo pipefail
if [[ -n "$LOCAL_REPO_REPLAY_VALIDATION_TIME" && -n "${FAKETIME_LIB:-}" ]]; then
faketime_value="$(python3 - <<'"'"'PY'"'"' "$LOCAL_REPO_REPLAY_VALIDATION_TIME"
from datetime import datetime, timezone
import sys
dt = datetime.fromisoformat(sys.argv[1].replace("Z", "+00:00")).astimezone(timezone.utc)
print("@" + dt.strftime("%Y-%m-%d %H:%M:%S"))
PY
)"
export LD_PRELOAD="$FAKETIME_LIB"
export TZ=UTC
unset FAKETIME_FMT
export FAKETIME="$faketime_value"
export FAKETIME_DONT_FAKE_MONOTONIC=1
fi
"$@" update --complete >"$LOCAL_REPO_REPLAY_OUT_DIR/update.log" 2>&1 || true
"$@" vrps -o "$LOCAL_REPO_REPLAY_OUT_DIR/vrps.csv" >"$LOCAL_REPO_REPLAY_OUT_DIR/vrps.log" 2>&1
"$@" vrps -f jsonext -o "$LOCAL_REPO_REPLAY_OUT_DIR/routinator.json" >"$LOCAL_REPO_REPLAY_OUT_DIR/json.log" 2>&1
' local_repo_replay "${ARGS[@]}"
python3 "$SCRIPT_DIR/normalize_rp_outputs.py" --kind routinator --input "$OUT_DIR/routinator.json" --vrps-out "$OUT_DIR/vrps.normalized.txt" --vaps-out "$OUT_DIR/vaps.normalized.txt"
python3 "$SCRIPT_DIR/summarize_replay.py" --rp routinator --out-dir "$OUT_DIR" --summary "$OUT_DIR/summary.json"
echo "done: $OUT_DIR"

Some files were not shown because too many files have changed in this diff Show More