增加 payload replay for snapshot,20260313 迭代
This commit is contained in:
100
scripts/payload_replay/README.md
Normal file
100
scripts/payload_replay/README.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Payload Replay Scripts
|
||||
|
||||
本目录提供基于本地 payload archive 的手工 replay 入口。
|
||||
|
||||
## `run_apnic_replay.sh`
|
||||
|
||||
默认使用:
|
||||
|
||||
- `tests/fixtures/tal/apnic-rfc7730-https.tal`
|
||||
- `tests/fixtures/ta/apnic-ta.cer`
|
||||
- `target/live/payload_replay/payload-archive`
|
||||
- `target/live/payload_replay/locks.json`
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
./scripts/payload_replay/run_apnic_replay.sh
|
||||
```
|
||||
|
||||
产物默认输出到:
|
||||
|
||||
- `target/live/payload_replay_runs/`
|
||||
|
||||
包含:
|
||||
|
||||
- replay DB 目录
|
||||
- `report.json`
|
||||
- `run.log`
|
||||
- `meta.json`
|
||||
- `summary.md`
|
||||
|
||||
## 环境变量
|
||||
|
||||
可覆盖:
|
||||
|
||||
- `TAL_PATH`
|
||||
- `TA_PATH`
|
||||
- `PAYLOAD_REPLAY_ARCHIVE`
|
||||
- `PAYLOAD_REPLAY_LOCKS`
|
||||
- `VALIDATION_TIME`
|
||||
- `MAX_DEPTH`
|
||||
- `MAX_INSTANCES`
|
||||
- `OUT_DIR`
|
||||
- `RUN_NAME`
|
||||
- `DB_DIR`
|
||||
- `REPORT_JSON`
|
||||
- `RUN_LOG`
|
||||
- `META_JSON`
|
||||
- `SUMMARY_MD`
|
||||
|
||||
## 说明
|
||||
|
||||
- 该脚本依赖 `rpki` CLI 已支持:
|
||||
- `--payload-replay-archive`
|
||||
- `--payload-replay-locks`
|
||||
- replay 模式必须搭配离线 TAL/TA 输入,不会去访问真实 RRDP / rsync 网络源。
|
||||
|
||||
## `report_to_routinator_csv.py`
|
||||
|
||||
把 `rpki` 生成的 `report.json` 转成 Routinator 风格的 VRP CSV:
|
||||
|
||||
```bash
|
||||
python3 scripts/payload_replay/report_to_routinator_csv.py \
|
||||
--report target/live/payload_replay_runs/<run>_report.json \
|
||||
--out target/live/payload_replay_runs/<run>_vrps.csv \
|
||||
--trust-anchor apnic
|
||||
```
|
||||
|
||||
输出列为:
|
||||
|
||||
- `ASN`
|
||||
- `IP Prefix`
|
||||
- `Max Length`
|
||||
- `Trust Anchor`
|
||||
|
||||
## `compare_with_routinator_record.sh`
|
||||
|
||||
把 ours 生成的 VRP CSV 与 Routinator 的 `record.csv` 做对比:
|
||||
|
||||
```bash
|
||||
./scripts/payload_replay/compare_with_routinator_record.sh \
|
||||
target/live/payload_replay_runs/<run>_vrps.csv \
|
||||
target/live/payload_replay/record.csv
|
||||
```
|
||||
|
||||
会产出:
|
||||
|
||||
- compare summary Markdown
|
||||
- `only_in_ours.csv`
|
||||
- `only_in_record.csv`
|
||||
|
||||
## `run_apnic_replay.sh` 现有额外产物
|
||||
|
||||
脚本现在除了 `report/meta/summary`,还会额外生成:
|
||||
|
||||
- `vrps.csv`
|
||||
- 若 `ROUTINATOR_RECORD_CSV` 存在,则生成:
|
||||
- compare summary
|
||||
- `only_in_ours.csv`
|
||||
- `only_in_record.csv`
|
||||
110
scripts/payload_replay/compare_with_routinator_record.sh
Executable file
110
scripts/payload_replay/compare_with_routinator_record.sh
Executable file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 2 || $# -gt 5 ]]; then
|
||||
echo "Usage: $0 <ours.csv> <record.csv> [summary.md] [only_in_ours.csv] [only_in_record.csv]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
OURS_CSV="$1"
|
||||
RECORD_CSV="$2"
|
||||
SUMMARY_MD="${3:-}"
|
||||
ONLY_IN_OURS_CSV="${4:-}"
|
||||
ONLY_IN_RECORD_CSV="${5:-}"
|
||||
|
||||
if [[ -z "$SUMMARY_MD" ]]; then
|
||||
SUMMARY_MD="$(dirname "$OURS_CSV")/$(basename "$OURS_CSV" .csv)_vs_routinator_summary.md"
|
||||
fi
|
||||
if [[ -z "$ONLY_IN_OURS_CSV" ]]; then
|
||||
ONLY_IN_OURS_CSV="$(dirname "$OURS_CSV")/$(basename "$OURS_CSV" .csv)_only_in_ours.csv"
|
||||
fi
|
||||
if [[ -z "$ONLY_IN_RECORD_CSV" ]]; then
|
||||
ONLY_IN_RECORD_CSV="$(dirname "$OURS_CSV")/$(basename "$OURS_CSV" .csv)_only_in_record.csv"
|
||||
fi
|
||||
|
||||
python3 - "$OURS_CSV" "$RECORD_CSV" "$SUMMARY_MD" "$ONLY_IN_OURS_CSV" "$ONLY_IN_RECORD_CSV" <<'PY'
|
||||
import csv
|
||||
import ipaddress
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ours_csv = Path(sys.argv[1])
|
||||
record_csv = Path(sys.argv[2])
|
||||
summary_md = Path(sys.argv[3])
|
||||
only_in_ours_csv = Path(sys.argv[4])
|
||||
only_in_record_csv = Path(sys.argv[5])
|
||||
|
||||
|
||||
def normalize_row(row: dict):
|
||||
asn = row["ASN"].strip().upper()
|
||||
prefix = row["IP Prefix"].strip()
|
||||
max_len = str(int(row["Max Length"]))
|
||||
ta = row["Trust Anchor"].strip()
|
||||
network = ipaddress.ip_network(prefix, strict=False)
|
||||
return {
|
||||
"ASN": asn,
|
||||
"IP Prefix": str(network),
|
||||
"Max Length": max_len,
|
||||
"Trust Anchor": ta,
|
||||
}
|
||||
|
||||
|
||||
def read_rows(path: Path):
|
||||
with path.open(encoding="utf-8", newline="") as f:
|
||||
rows = [normalize_row(r) for r in csv.DictReader(f)]
|
||||
return rows
|
||||
|
||||
|
||||
def row_key(row: dict):
|
||||
network = ipaddress.ip_network(row["IP Prefix"], strict=False)
|
||||
return (
|
||||
row["ASN"],
|
||||
network.version,
|
||||
int(network.network_address),
|
||||
network.prefixlen,
|
||||
int(row["Max Length"]),
|
||||
row["Trust Anchor"],
|
||||
)
|
||||
|
||||
|
||||
def write_rows(path: Path, rows):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8", newline="") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=["ASN", "IP Prefix", "Max Length", "Trust Anchor"])
|
||||
writer.writeheader()
|
||||
for row in rows:
|
||||
writer.writerow(row)
|
||||
|
||||
ours = read_rows(ours_csv)
|
||||
record = read_rows(record_csv)
|
||||
ours_map = {row_key(r): r for r in ours}
|
||||
record_map = {row_key(r): r for r in record}
|
||||
only_in_ours = [ours_map[k] for k in sorted(set(ours_map) - set(record_map))]
|
||||
only_in_record = [record_map[k] for k in sorted(set(record_map) - set(ours_map))]
|
||||
intersection = len(set(ours_map) & set(record_map))
|
||||
|
||||
write_rows(only_in_ours_csv, only_in_ours)
|
||||
write_rows(only_in_record_csv, only_in_record)
|
||||
|
||||
summary_md.parent.mkdir(parents=True, exist_ok=True)
|
||||
summary = []
|
||||
summary.append("# Replay vs Routinator VRP Compare\n\n")
|
||||
summary.append(f"- ours_csv: `{ours_csv}`\n")
|
||||
summary.append(f"- record_csv: `{record_csv}`\n")
|
||||
summary.append(f"- only_in_ours_csv: `{only_in_ours_csv}`\n")
|
||||
summary.append(f"- only_in_record_csv: `{only_in_record_csv}`\n\n")
|
||||
summary.append("| metric | value |\n")
|
||||
summary.append("|---|---:|\n")
|
||||
summary.append(f"| ours_total | {len(ours_map)} |\n")
|
||||
summary.append(f"| record_total | {len(record_map)} |\n")
|
||||
summary.append(f"| intersection | {intersection} |\n")
|
||||
summary.append(f"| only_in_ours | {len(only_in_ours)} |\n")
|
||||
summary.append(f"| only_in_record | {len(only_in_record)} |\n")
|
||||
summary_md.write_text("".join(summary), encoding="utf-8")
|
||||
print(summary_md)
|
||||
PY
|
||||
|
||||
echo "== compare complete ==" >&2
|
||||
echo "- summary: $SUMMARY_MD" >&2
|
||||
echo "- only_in_ours: $ONLY_IN_OURS_CSV" >&2
|
||||
echo "- only_in_record: $ONLY_IN_RECORD_CSV" >&2
|
||||
57
scripts/payload_replay/report_to_routinator_csv.py
Executable file
57
scripts/payload_replay/report_to_routinator_csv.py
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import csv
|
||||
import ipaddress
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(
|
||||
description="Convert rpki report.json VRPs into Routinator-compatible CSV"
|
||||
)
|
||||
p.add_argument("--report", required=True, help="path to rpki report.json")
|
||||
p.add_argument("--out", required=True, help="output CSV path")
|
||||
p.add_argument(
|
||||
"--trust-anchor",
|
||||
default="unknown",
|
||||
help="Trust Anchor column value (default: unknown)",
|
||||
)
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def sort_key(vrp: dict):
|
||||
network = ipaddress.ip_network(vrp["prefix"], strict=False)
|
||||
return (
|
||||
int(vrp["asn"]),
|
||||
network.version,
|
||||
int(network.network_address),
|
||||
network.prefixlen,
|
||||
int(vrp["max_length"]),
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
report = json.loads(Path(args.report).read_text(encoding="utf-8"))
|
||||
vrps = list(report.get("vrps") or [])
|
||||
vrps.sort(key=sort_key)
|
||||
|
||||
out_path = Path(args.out)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with out_path.open("w", encoding="utf-8", newline="") as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow(["ASN", "IP Prefix", "Max Length", "Trust Anchor"])
|
||||
for vrp in vrps:
|
||||
w.writerow([
|
||||
f"AS{vrp['asn']}",
|
||||
vrp["prefix"],
|
||||
vrp["max_length"],
|
||||
args.trust_anchor,
|
||||
])
|
||||
print(out_path)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
150
scripts/payload_replay/run_apnic_replay.sh
Executable file
150
scripts/payload_replay/run_apnic_replay.sh
Executable file
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
TAL_PATH="${TAL_PATH:-$ROOT_DIR/tests/fixtures/tal/apnic-rfc7730-https.tal}"
|
||||
TA_PATH="${TA_PATH:-$ROOT_DIR/tests/fixtures/ta/apnic-ta.cer}"
|
||||
PAYLOAD_REPLAY_ARCHIVE="${PAYLOAD_REPLAY_ARCHIVE:-$ROOT_DIR/target/live/payload_replay/payload-archive}"
|
||||
PAYLOAD_REPLAY_LOCKS="${PAYLOAD_REPLAY_LOCKS:-$ROOT_DIR/target/live/payload_replay/locks.json}"
|
||||
VALIDATION_TIME="${VALIDATION_TIME:-2026-03-13T02:30:00Z}"
|
||||
TRUST_ANCHOR="${TRUST_ANCHOR:-apnic}"
|
||||
ROUTINATOR_RECORD_CSV="${ROUTINATOR_RECORD_CSV:-$ROOT_DIR/target/live/payload_replay/record.csv}"
|
||||
MAX_DEPTH="${MAX_DEPTH:-}"
|
||||
MAX_INSTANCES="${MAX_INSTANCES:-}"
|
||||
OUT_DIR="${OUT_DIR:-$ROOT_DIR/target/live/payload_replay_runs}"
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
TS="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
RUN_NAME="${RUN_NAME:-apnic_replay_${TS}}"
|
||||
DB_DIR="${DB_DIR:-$OUT_DIR/${RUN_NAME}_db}"
|
||||
REPORT_JSON="${REPORT_JSON:-$OUT_DIR/${RUN_NAME}_report.json}"
|
||||
RUN_LOG="${RUN_LOG:-$OUT_DIR/${RUN_NAME}_run.log}"
|
||||
META_JSON="${META_JSON:-$OUT_DIR/${RUN_NAME}_meta.json}"
|
||||
SUMMARY_MD="${SUMMARY_MD:-$OUT_DIR/${RUN_NAME}_summary.md}"
|
||||
VRPS_CSV="${VRPS_CSV:-$OUT_DIR/${RUN_NAME}_vrps.csv}"
|
||||
COMPARE_SUMMARY_MD="${COMPARE_SUMMARY_MD:-$OUT_DIR/${RUN_NAME}_compare_summary.md}"
|
||||
ONLY_IN_OURS_CSV="${ONLY_IN_OURS_CSV:-$OUT_DIR/${RUN_NAME}_only_in_ours.csv}"
|
||||
ONLY_IN_RECORD_CSV="${ONLY_IN_RECORD_CSV:-$OUT_DIR/${RUN_NAME}_only_in_record.csv}"
|
||||
|
||||
cmd=(cargo run --release --bin rpki --
|
||||
--db "$DB_DIR"
|
||||
--tal-path "$TAL_PATH"
|
||||
--ta-path "$TA_PATH"
|
||||
--payload-replay-archive "$PAYLOAD_REPLAY_ARCHIVE"
|
||||
--payload-replay-locks "$PAYLOAD_REPLAY_LOCKS"
|
||||
--validation-time "$VALIDATION_TIME"
|
||||
--report-json "$REPORT_JSON")
|
||||
|
||||
if [[ -n "$MAX_DEPTH" ]]; then
|
||||
cmd+=(--max-depth "$MAX_DEPTH")
|
||||
fi
|
||||
if [[ -n "$MAX_INSTANCES" ]]; then
|
||||
cmd+=(--max-instances "$MAX_INSTANCES")
|
||||
fi
|
||||
|
||||
run_start_s="$(date +%s)"
|
||||
(
|
||||
echo "# command:"
|
||||
printf '%q ' "${cmd[@]}"
|
||||
echo
|
||||
echo
|
||||
"${cmd[@]}"
|
||||
) 2>&1 | tee "$RUN_LOG" >/dev/null
|
||||
run_end_s="$(date +%s)"
|
||||
run_duration_s="$((run_end_s - run_start_s))"
|
||||
|
||||
TAL_PATH="$TAL_PATH" \
|
||||
TA_PATH="$TA_PATH" \
|
||||
PAYLOAD_REPLAY_ARCHIVE="$PAYLOAD_REPLAY_ARCHIVE" \
|
||||
PAYLOAD_REPLAY_LOCKS="$PAYLOAD_REPLAY_LOCKS" \
|
||||
DB_DIR="$DB_DIR" \
|
||||
REPORT_JSON="$REPORT_JSON" \
|
||||
RUN_LOG="$RUN_LOG" \
|
||||
VALIDATION_TIME="$VALIDATION_TIME" \
|
||||
RUN_DURATION_S="$run_duration_s" \
|
||||
python3 - "$REPORT_JSON" "$META_JSON" "$SUMMARY_MD" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
report_path = Path(sys.argv[1])
|
||||
meta_path = Path(sys.argv[2])
|
||||
summary_path = Path(sys.argv[3])
|
||||
rep = json.loads(report_path.read_text(encoding="utf-8"))
|
||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
meta = {
|
||||
"recorded_at_utc": now,
|
||||
"tal_path": os.environ["TAL_PATH"],
|
||||
"ta_path": os.environ["TA_PATH"],
|
||||
"payload_replay_archive": os.environ["PAYLOAD_REPLAY_ARCHIVE"],
|
||||
"payload_replay_locks": os.environ["PAYLOAD_REPLAY_LOCKS"],
|
||||
"db_dir": os.environ["DB_DIR"],
|
||||
"report_json": os.environ["REPORT_JSON"],
|
||||
"run_log": os.environ["RUN_LOG"],
|
||||
"validation_time_arg": os.environ["VALIDATION_TIME"],
|
||||
"durations_secs": {
|
||||
"rpki_run": int(os.environ["RUN_DURATION_S"]),
|
||||
},
|
||||
"counts": {
|
||||
"publication_points_processed": rep["tree"]["instances_processed"],
|
||||
"publication_points_failed": rep["tree"]["instances_failed"],
|
||||
"vrps": len(rep["vrps"]),
|
||||
"aspas": len(rep["aspas"]),
|
||||
"audit_publication_points": len(rep["publication_points"]),
|
||||
},
|
||||
}
|
||||
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
summary = []
|
||||
summary.append("# Payload Replay Summary\n\n")
|
||||
summary.append(f"- recorded_at_utc: `{now}`\n")
|
||||
summary.append(f"- tal_path: `{meta['tal_path']}`\n")
|
||||
summary.append(f"- ta_path: `{meta['ta_path']}`\n")
|
||||
summary.append(f"- payload_replay_archive: `{meta['payload_replay_archive']}`\n")
|
||||
summary.append(f"- payload_replay_locks: `{meta['payload_replay_locks']}`\n")
|
||||
summary.append(f"- db: `{meta['db_dir']}`\n")
|
||||
summary.append(f"- report_json: `{meta['report_json']}`\n")
|
||||
summary.append(f"- validation_time_arg: `{meta['validation_time_arg']}`\n\n")
|
||||
summary.append("## Results\n\n")
|
||||
summary.append("| metric | value |\n")
|
||||
summary.append("|---|---:|\n")
|
||||
for k, v in meta["counts"].items():
|
||||
summary.append(f"| {k} | {v} |\n")
|
||||
summary.append("\n## Durations\n\n")
|
||||
summary.append("| step | seconds |\n")
|
||||
summary.append("|---|---:|\n")
|
||||
for k, v in meta["durations_secs"].items():
|
||||
summary.append(f"| {k} | {v} |\n")
|
||||
summary_path.write_text("".join(summary), encoding="utf-8")
|
||||
print(summary_path)
|
||||
PY
|
||||
|
||||
python3 scripts/payload_replay/report_to_routinator_csv.py \
|
||||
--report "$REPORT_JSON" \
|
||||
--out "$VRPS_CSV" \
|
||||
--trust-anchor "$TRUST_ANCHOR" >/dev/null
|
||||
|
||||
if [[ -f "$ROUTINATOR_RECORD_CSV" ]]; then
|
||||
./scripts/payload_replay/compare_with_routinator_record.sh \
|
||||
"$VRPS_CSV" \
|
||||
"$ROUTINATOR_RECORD_CSV" \
|
||||
"$COMPARE_SUMMARY_MD" \
|
||||
"$ONLY_IN_OURS_CSV" \
|
||||
"$ONLY_IN_RECORD_CSV" >/dev/null
|
||||
fi
|
||||
|
||||
echo "== payload replay run complete ==" >&2
|
||||
echo "- db: $DB_DIR" >&2
|
||||
echo "- report: $REPORT_JSON" >&2
|
||||
echo "- run log: $RUN_LOG" >&2
|
||||
echo "- meta json: $META_JSON" >&2
|
||||
echo "- summary md: $SUMMARY_MD" >&2
|
||||
echo "- vrps csv: $VRPS_CSV" >&2
|
||||
if [[ -f "$COMPARE_SUMMARY_MD" ]]; then
|
||||
echo "- compare summary: $COMPARE_SUMMARY_MD" >&2
|
||||
echo "- only in ours: $ONLY_IN_OURS_CSV" >&2
|
||||
echo "- only in record: $ONLY_IN_RECORD_CSV" >&2
|
||||
fi
|
||||
Reference in New Issue
Block a user