20260401 live recorder 扩展到1+N个delta,完成5个RIR录制,以及三方replay,结果三方均对齐vrps和vaps
This commit is contained in:
262
src/bin/measure_sequence_replay.rs
Normal file
262
src/bin/measure_sequence_replay.rs
Normal file
@@ -0,0 +1,262 @@
|
||||
use rpki::bundle::record_io::load_validation_time;
|
||||
use rpki::storage::RocksStore;
|
||||
use rpki::validation::run_tree_from_tal::{
|
||||
run_tree_from_tal_and_ta_der_payload_delta_replay_step_serial_audit,
|
||||
run_tree_from_tal_and_ta_der_payload_replay_serial_audit,
|
||||
};
|
||||
use rpki::validation::tree::TreeRunConfig;
|
||||
use serde::Serialize;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Instant;
|
||||
|
||||
fn usage() -> &'static str {
|
||||
"Usage: measure_sequence_replay --bundle-root <dir> [--rir <rir[,rir...]>] --out <path> [--keep-db]"
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Args {
|
||||
bundle_root: Option<PathBuf>,
|
||||
rirs: Option<Vec<String>>,
|
||||
out: Option<PathBuf>,
|
||||
keep_db: bool,
|
||||
}
|
||||
|
||||
fn parse_args() -> Result<Args, String> {
|
||||
let mut out = Args::default();
|
||||
let argv: Vec<String> = std::env::args().skip(1).collect();
|
||||
let mut i = 0usize;
|
||||
while i < argv.len() {
|
||||
match argv[i].as_str() {
|
||||
"--bundle-root" => {
|
||||
i += 1;
|
||||
out.bundle_root = Some(PathBuf::from(
|
||||
argv.get(i).ok_or("--bundle-root requires a value")?,
|
||||
));
|
||||
}
|
||||
"--rir" => {
|
||||
i += 1;
|
||||
let value = argv.get(i).ok_or("--rir requires a value")?;
|
||||
out.rirs = Some(
|
||||
value
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_lowercase())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
"--out" => {
|
||||
i += 1;
|
||||
out.out = Some(PathBuf::from(argv.get(i).ok_or("--out requires a value")?));
|
||||
}
|
||||
"--keep-db" => out.keep_db = true,
|
||||
"--help" | "-h" => return Err(usage().to_string()),
|
||||
other => return Err(format!("unknown argument: {other}\n{}", usage())),
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if out.bundle_root.is_none() || out.out.is_none() {
|
||||
return Err(format!(
|
||||
"--bundle-root and --out are required\n{}",
|
||||
usage()
|
||||
));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PhaseTiming {
|
||||
duration_seconds: f64,
|
||||
vrp_count: usize,
|
||||
vap_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RirTiming {
|
||||
rir: String,
|
||||
base: PhaseTiming,
|
||||
steps: Vec<(String, PhaseTiming)>,
|
||||
}
|
||||
|
||||
fn discover_rirs(bundle_root: &Path) -> Result<Vec<String>, String> {
|
||||
let mut out = Vec::new();
|
||||
for entry in fs::read_dir(bundle_root)
|
||||
.map_err(|e| format!("read_dir failed: {}: {e}", bundle_root.display()))?
|
||||
{
|
||||
let entry = entry.map_err(|e| format!("read_dir entry failed: {e}"))?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() && path.join("bundle.json").exists() && path.join("tal.tal").exists() {
|
||||
out.push(
|
||||
path.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.ok_or_else(|| format!("invalid rir dir name: {}", path.display()))?
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
out.sort();
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn path_join(root: &Path, relative: &str) -> PathBuf {
|
||||
root.join(relative)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = real_main() {
|
||||
eprintln!("{err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn real_main() -> Result<(), String> {
|
||||
let args = parse_args()?;
|
||||
let bundle_root = args.bundle_root.unwrap();
|
||||
let out_path = args.out.unwrap();
|
||||
let rirs = match args.rirs {
|
||||
Some(v) => v,
|
||||
None => discover_rirs(&bundle_root)?,
|
||||
};
|
||||
let mut results = Vec::new();
|
||||
let tmp_root = out_path
|
||||
.parent()
|
||||
.unwrap_or_else(|| Path::new("."))
|
||||
.join(".tmp-sequence-replay");
|
||||
fs::create_dir_all(&tmp_root)
|
||||
.map_err(|e| format!("create tmp root failed: {}: {e}", tmp_root.display()))?;
|
||||
|
||||
for rir in rirs {
|
||||
let rir_dir = bundle_root.join(&rir);
|
||||
let bundle: serde_json::Value = serde_json::from_slice(
|
||||
&fs::read(rir_dir.join("bundle.json"))
|
||||
.map_err(|e| format!("read bundle failed: {}: {e}", rir_dir.display()))?,
|
||||
)
|
||||
.map_err(|e| format!("parse bundle failed for {}: {e}", rir_dir.display()))?;
|
||||
let tal_bytes = fs::read(rir_dir.join("tal.tal"))
|
||||
.map_err(|e| format!("read tal.tal failed for {}: {e}", rir_dir.display()))?;
|
||||
let ta_bytes = fs::read(rir_dir.join("ta.cer"))
|
||||
.map_err(|e| format!("read ta.cer failed for {}: {e}", rir_dir.display()))?;
|
||||
|
||||
let db_dir = tmp_root.join(format!("{rir}-db"));
|
||||
if db_dir.exists() {
|
||||
fs::remove_dir_all(&db_dir)
|
||||
.map_err(|e| format!("remove old db failed: {}: {e}", db_dir.display()))?;
|
||||
}
|
||||
let store =
|
||||
RocksStore::open(&db_dir).map_err(|e| format!("open rocksdb failed for {rir}: {e}"))?;
|
||||
|
||||
let base_archive = path_join(
|
||||
&rir_dir,
|
||||
bundle["base"]["relativeArchivePath"]
|
||||
.as_str()
|
||||
.ok_or("bundle missing base.relativeArchivePath")?,
|
||||
);
|
||||
let base_locks = path_join(
|
||||
&rir_dir,
|
||||
bundle["base"]["relativeLocksPath"]
|
||||
.as_str()
|
||||
.ok_or("bundle missing base.relativeLocksPath")?,
|
||||
);
|
||||
let base_validation_time = load_validation_time(&base_locks)
|
||||
.map_err(|e| format!("load base validation time failed for {rir}: {e}"))?;
|
||||
|
||||
let start = Instant::now();
|
||||
let base_out = run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
|
||||
&store,
|
||||
&rpki::policy::Policy::default(),
|
||||
&tal_bytes,
|
||||
&ta_bytes,
|
||||
None,
|
||||
&base_archive,
|
||||
&base_locks,
|
||||
base_validation_time,
|
||||
&TreeRunConfig {
|
||||
max_depth: None,
|
||||
max_instances: None,
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("base replay failed for {rir}: {e}"))?;
|
||||
let base_timing = PhaseTiming {
|
||||
duration_seconds: start.elapsed().as_secs_f64(),
|
||||
vrp_count: base_out.tree.vrps.len(),
|
||||
vap_count: base_out.tree.aspas.len(),
|
||||
};
|
||||
|
||||
let mut previous_locks = base_locks.clone();
|
||||
let mut step_timings = Vec::new();
|
||||
for step in bundle["deltaSequence"]["steps"]
|
||||
.as_array()
|
||||
.ok_or("bundle missing deltaSequence.steps")?
|
||||
{
|
||||
let step_id = step["id"]
|
||||
.as_str()
|
||||
.ok_or("step missing id")?
|
||||
.to_string();
|
||||
let step_dir = path_join(
|
||||
&rir_dir,
|
||||
step["relativePath"]
|
||||
.as_str()
|
||||
.ok_or("step missing relativePath")?,
|
||||
);
|
||||
let delta_archive = path_join(
|
||||
&rir_dir,
|
||||
step["relativeArchivePath"]
|
||||
.as_str()
|
||||
.ok_or("step missing relativeArchivePath")?,
|
||||
);
|
||||
let delta_locks = path_join(
|
||||
&rir_dir,
|
||||
step["relativeTransitionLocksPath"]
|
||||
.as_str()
|
||||
.ok_or("step missing relativeTransitionLocksPath")?,
|
||||
);
|
||||
let validation_time = load_validation_time(&delta_locks)
|
||||
.map_err(|e| format!("load step validation time failed for {rir}/{step_id}: {e}"))?;
|
||||
let start = Instant::now();
|
||||
let step_out = run_tree_from_tal_and_ta_der_payload_delta_replay_step_serial_audit(
|
||||
&store,
|
||||
&rpki::policy::Policy::default(),
|
||||
&tal_bytes,
|
||||
&ta_bytes,
|
||||
None,
|
||||
&delta_archive,
|
||||
&previous_locks,
|
||||
&delta_locks,
|
||||
validation_time,
|
||||
&TreeRunConfig {
|
||||
max_depth: None,
|
||||
max_instances: None,
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("delta step replay failed for {rir}/{step_id}: {e}"))?;
|
||||
step_timings.push((
|
||||
step_id.clone(),
|
||||
PhaseTiming {
|
||||
duration_seconds: start.elapsed().as_secs_f64(),
|
||||
vrp_count: step_out.tree.vrps.len(),
|
||||
vap_count: step_out.tree.aspas.len(),
|
||||
},
|
||||
));
|
||||
previous_locks = step_dir.join("target-locks.json");
|
||||
}
|
||||
|
||||
results.push(RirTiming {
|
||||
rir,
|
||||
base: base_timing,
|
||||
steps: step_timings,
|
||||
});
|
||||
|
||||
if !args.keep_db && db_dir.exists() {
|
||||
fs::remove_dir_all(&db_dir)
|
||||
.map_err(|e| format!("remove db failed: {}: {e}", db_dir.display()))?;
|
||||
}
|
||||
}
|
||||
|
||||
fs::write(
|
||||
&out_path,
|
||||
serde_json::to_vec_pretty(&results).map_err(|e| format!("encode json failed: {e}"))?,
|
||||
)
|
||||
.map_err(|e| format!("write out failed: {}: {e}", out_path.display()))?;
|
||||
println!("{}", out_path.display());
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
use rpki::bundle::{
|
||||
BundleManifest, BundleManifestEntry, RirBundleMetadata, RecordingHttpFetcher,
|
||||
RecordingRsyncFetcher, build_vap_compare_rows, build_vrp_compare_rows,
|
||||
write_live_base_replay_bundle_inputs, write_vap_csv, write_vrp_csv,
|
||||
RirBundleMetadata, RecordingHttpFetcher, RecordingRsyncFetcher,
|
||||
build_single_rir_bundle_manifest, build_vap_compare_rows, build_vrp_compare_rows,
|
||||
sha256_hex, write_json, write_live_base_replay_bundle_inputs, write_live_bundle_rir_readme,
|
||||
write_live_bundle_top_readme, write_timing_json, write_vap_csv, write_vrp_csv,
|
||||
};
|
||||
use rpki::ccr::{build_ccr_from_run, verify_content_info, write_ccr_file};
|
||||
use rpki::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
|
||||
@@ -13,9 +14,8 @@ use rpki::validation::run_tree_from_tal::{
|
||||
run_tree_from_tal_and_ta_der_serial_audit,
|
||||
};
|
||||
use rpki::validation::tree::TreeRunConfig;
|
||||
use sha2::Digest;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
|
||||
@@ -135,57 +135,6 @@ fn parse_args(argv: &[String]) -> Result<Args, String> {
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
fn sha256_hex(bytes: &[u8]) -> String {
|
||||
hex::encode(sha2::Sha256::digest(bytes))
|
||||
}
|
||||
|
||||
fn write_json(path: &Path, value: &impl serde::Serialize) -> Result<(), String> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
|
||||
}
|
||||
let bytes = serde_json::to_vec_pretty(value).map_err(|e| e.to_string())?;
|
||||
fs::write(path, bytes).map_err(|e| format!("write json failed: {}: {e}", path.display()))
|
||||
}
|
||||
|
||||
fn write_timing_json(
|
||||
path: &Path,
|
||||
mode: &str,
|
||||
validation_time: &time::OffsetDateTime,
|
||||
duration: std::time::Duration,
|
||||
) -> Result<(), String> {
|
||||
write_json(
|
||||
path,
|
||||
&serde_json::json!({
|
||||
"mode": mode,
|
||||
"validationTime": validation_time
|
||||
.format(&Rfc3339)
|
||||
.map_err(|e| format!("format validation time failed: {e}"))?,
|
||||
"durationSeconds": duration.as_secs_f64(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn write_top_readme(path: &Path, rir: &str) -> Result<(), String> {
|
||||
fs::write(
|
||||
path,
|
||||
format!(
|
||||
"# Ours Live Replay Bundle\n\nThis run contains one per-RIR bundle recorded online by `ours`.\n\n- RIR: `{rir}`\n- Reference result format: `CCR`\n"
|
||||
),
|
||||
)
|
||||
.map_err(|e| format!("write readme failed: {}: {e}", path.display()))
|
||||
}
|
||||
|
||||
fn write_rir_readme(path: &Path, rir: &str, base_validation_time: &str) -> Result<(), String> {
|
||||
fs::write(
|
||||
path,
|
||||
format!(
|
||||
"# {rir} live replay bundle\n\n- `tal.tal` and `ta.cer` are the actual live run inputs.\n- `base-locks.json.validationTime` = `{base_validation_time}`.\n- `base.ccr` is the authoritative reference result.\n- `base-vrps.csv` and `base-vaps.csv` are compare views derived from `base.ccr`.\n"
|
||||
),
|
||||
)
|
||||
.map_err(|e| format!("write rir readme failed: {}: {e}", path.display()))
|
||||
}
|
||||
|
||||
fn run(args: Args) -> Result<PathBuf, String> {
|
||||
let rir = args.rir.as_ref().unwrap();
|
||||
let rir_normalized = rir.to_ascii_lowercase();
|
||||
@@ -370,25 +319,18 @@ fn run(args: Args) -> Result<PathBuf, String> {
|
||||
}
|
||||
}),
|
||||
)?;
|
||||
write_top_readme(&out_root.join("README.md"), &rir_normalized)?;
|
||||
write_rir_readme(&rir_dir.join("README.md"), &rir_normalized, &metadata.base_validation_time)?;
|
||||
write_live_bundle_top_readme(&out_root.join("README.md"), &rir_normalized)?;
|
||||
write_live_bundle_rir_readme(&rir_dir.join("README.md"), &rir_normalized, &metadata.base_validation_time)?;
|
||||
write_json(
|
||||
&out_root.join("bundle-manifest.json"),
|
||||
&BundleManifest {
|
||||
schema_version: "20260330-v1".to_string(),
|
||||
bundle_producer: "ours".to_string(),
|
||||
recorded_at_rfc3339_utc: time::OffsetDateTime::now_utc()
|
||||
.format(&Rfc3339)
|
||||
.map_err(|e| format!("format recorded_at failed: {e}"))?,
|
||||
rirs: vec![rir_normalized.clone()],
|
||||
per_rir_bundles: vec![BundleManifestEntry {
|
||||
rir: rir_normalized.clone(),
|
||||
relative_path: rir_normalized,
|
||||
base_validation_time: metadata.base_validation_time.clone(),
|
||||
delta_validation_time: None,
|
||||
has_aspa: metadata.has_aspa,
|
||||
}],
|
||||
},
|
||||
&build_single_rir_bundle_manifest(
|
||||
"20260330-v1",
|
||||
"ours",
|
||||
&rir_normalized,
|
||||
&validation_time,
|
||||
None,
|
||||
metadata.has_aspa,
|
||||
)?,
|
||||
)?;
|
||||
|
||||
let _ = fs::remove_dir_all(&db_dir);
|
||||
@@ -424,7 +366,7 @@ mod tests {
|
||||
];
|
||||
let args = parse_args(&argv).expect("parse");
|
||||
assert_eq!(args.rir.as_deref(), Some("apnic"));
|
||||
assert_eq!(args.out_dir.as_deref(), Some(Path::new("out")));
|
||||
assert_eq!(args.out_dir.as_deref(), Some(std::path::Path::new("out")));
|
||||
assert_eq!(args.http_timeout_secs, 20);
|
||||
assert_eq!(args.rsync_timeout_secs, 60);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use rpki::bundle::{
|
||||
BundleManifest, BundleManifestEntry, RecordingHttpFetcher,
|
||||
RecordingRsyncFetcher, build_vap_compare_rows, build_vrp_compare_rows,
|
||||
write_live_delta_replay_bundle_inputs, write_vap_csv, write_vrp_csv,
|
||||
RecordingHttpFetcher, RecordingRsyncFetcher, build_single_rir_bundle_manifest,
|
||||
build_vap_compare_rows, build_vrp_compare_rows, copy_dir_all, load_validation_time,
|
||||
sha256_hex, write_json, write_live_delta_replay_bundle_inputs, write_vap_csv, write_vrp_csv,
|
||||
};
|
||||
use rpki::ccr::{build_ccr_from_run, decode_content_info, verify_content_info, write_ccr_file};
|
||||
use rpki::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
|
||||
@@ -15,7 +15,6 @@ use rpki::validation::run_tree_from_tal::{
|
||||
run_tree_from_tal_and_ta_der_serial_audit,
|
||||
};
|
||||
use rpki::validation::tree::TreeRunConfig;
|
||||
use sha2::Digest;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Instant;
|
||||
@@ -130,56 +129,6 @@ fn parse_args(argv: &[String]) -> Result<Args, String> {
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
fn sha256_hex(bytes: &[u8]) -> String {
|
||||
hex::encode(sha2::Sha256::digest(bytes))
|
||||
}
|
||||
|
||||
fn write_json(path: &Path, value: &impl serde::Serialize) -> Result<(), String> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
|
||||
}
|
||||
let bytes = serde_json::to_vec_pretty(value).map_err(|e| e.to_string())?;
|
||||
fs::write(path, bytes).map_err(|e| format!("write json failed: {}: {e}", path.display()))
|
||||
}
|
||||
|
||||
fn copy_dir_all(src: &Path, dst: &Path) -> Result<(), String> {
|
||||
fs::create_dir_all(dst)
|
||||
.map_err(|e| format!("create directory failed: {}: {e}", dst.display()))?;
|
||||
for entry in fs::read_dir(src).map_err(|e| format!("read_dir failed: {}: {e}", src.display()))? {
|
||||
let entry = entry.map_err(|e| format!("read_dir entry failed: {}: {e}", src.display()))?;
|
||||
let ty = entry
|
||||
.file_type()
|
||||
.map_err(|e| format!("file_type failed: {}: {e}", entry.path().display()))?;
|
||||
let to = dst.join(entry.file_name());
|
||||
if ty.is_dir() {
|
||||
copy_dir_all(&entry.path(), &to)?;
|
||||
} else if ty.is_file() {
|
||||
if let Some(parent) = to.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
|
||||
}
|
||||
fs::copy(entry.path(), &to)
|
||||
.map_err(|e| format!("copy failed: {} -> {}: {e}", entry.path().display(), to.display()))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_validation_time(path: &Path) -> Result<time::OffsetDateTime, String> {
|
||||
let json: serde_json::Value = serde_json::from_slice(
|
||||
&fs::read(path).map_err(|e| format!("read json failed: {}: {e}", path.display()))?,
|
||||
)
|
||||
.map_err(|e| format!("parse json failed: {}: {e}", path.display()))?;
|
||||
let value = json
|
||||
.get("validationTime")
|
||||
.or_else(|| json.get("validation_time"))
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| format!("validationTime missing in {}", path.display()))?;
|
||||
time::OffsetDateTime::parse(value, &Rfc3339)
|
||||
.map_err(|e| format!("invalid validationTime in {}: {e}", path.display()))
|
||||
}
|
||||
|
||||
fn ensure_recorded_target_snapshots(
|
||||
store: &RocksStore,
|
||||
base_bundle_dir: &Path,
|
||||
@@ -466,27 +415,14 @@ fn run(args: Args) -> Result<PathBuf, String> {
|
||||
});
|
||||
write_json(&rir_dir.join("verification.json"), &verification_json)?;
|
||||
|
||||
let bundle_manifest = BundleManifest {
|
||||
schema_version: "20260330-v1".to_string(),
|
||||
bundle_producer: "ours".to_string(),
|
||||
recorded_at_rfc3339_utc: time::OffsetDateTime::now_utc()
|
||||
.format(&Rfc3339)
|
||||
.map_err(|e| format!("format recorded_at failed: {e}"))?,
|
||||
rirs: vec![rir_normalized.clone()],
|
||||
per_rir_bundles: vec![BundleManifestEntry {
|
||||
rir: rir_normalized.clone(),
|
||||
relative_path: rir_normalized,
|
||||
base_validation_time: base_validation_time
|
||||
.format(&Rfc3339)
|
||||
.map_err(|e| format!("format base validation time failed: {e}"))?,
|
||||
delta_validation_time: Some(
|
||||
target_validation_time
|
||||
.format(&Rfc3339)
|
||||
.map_err(|e| format!("format delta validation time failed: {e}"))?,
|
||||
),
|
||||
has_aspa: bundle_json["hasAspa"].as_bool().unwrap_or(false),
|
||||
}],
|
||||
};
|
||||
let bundle_manifest = build_single_rir_bundle_manifest(
|
||||
"20260330-v1",
|
||||
"ours",
|
||||
&rir_normalized,
|
||||
&base_validation_time,
|
||||
Some(&target_validation_time),
|
||||
bundle_json["hasAspa"].as_bool().unwrap_or(false),
|
||||
)?;
|
||||
write_json(&out_root.join("bundle-manifest.json"), &bundle_manifest)?;
|
||||
|
||||
let _ = fs::remove_dir_all(&target_store_dir);
|
||||
|
||||
929
src/bin/replay_bundle_capture_sequence.rs
Normal file
929
src/bin/replay_bundle_capture_sequence.rs
Normal file
@@ -0,0 +1,929 @@
|
||||
use rpki::bundle::{
|
||||
BaseBundleStateMetadataV2, BundleManifestEntryV2, BundleManifestV2, DeltaSequenceMetadataV2,
|
||||
DeltaStepMetadataV2, RecordingHttpFetcher, RecordingRsyncFetcher, RirBundleMetadataV2,
|
||||
build_vap_compare_rows, build_vrp_compare_rows, sha256_hex, write_current_replay_state_locks,
|
||||
write_json, write_live_base_replay_bundle_inputs, write_live_delta_replay_step_inputs,
|
||||
write_vap_csv, write_vrp_csv,
|
||||
};
|
||||
use rpki::ccr::{
|
||||
CcrVerifySummary, build_ccr_from_run, decode_content_info, verify_content_info, write_ccr_file,
|
||||
};
|
||||
use rpki::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
|
||||
use rpki::fetch::rsync_system::{SystemRsyncConfig, SystemRsyncFetcher};
|
||||
use rpki::policy::Policy;
|
||||
use rpki::storage::RocksStore;
|
||||
use rpki::sync::rrdp::Fetcher;
|
||||
use rpki::validation::run_tree_from_tal::{
|
||||
run_tree_from_tal_and_ta_der_payload_delta_replay_step_serial_audit,
|
||||
run_tree_from_tal_and_ta_der_payload_replay_serial_audit,
|
||||
run_tree_from_tal_and_ta_der_serial_audit,
|
||||
};
|
||||
use rpki::validation::tree::TreeRunConfig;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, Instant};
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
struct Args {
|
||||
rir: Option<String>,
|
||||
out_dir: Option<PathBuf>,
|
||||
tal_path: Option<PathBuf>,
|
||||
ta_path: Option<PathBuf>,
|
||||
base_validation_time: Option<time::OffsetDateTime>,
|
||||
delta_count: usize,
|
||||
delta_interval_secs: u64,
|
||||
http_timeout_secs: u64,
|
||||
rsync_timeout_secs: u64,
|
||||
rsync_mirror_root: Option<PathBuf>,
|
||||
max_depth: Option<usize>,
|
||||
max_instances: Option<usize>,
|
||||
trust_anchor: Option<String>,
|
||||
keep_db: bool,
|
||||
capture_inputs_only: bool,
|
||||
}
|
||||
|
||||
fn usage() -> &'static str {
|
||||
"Usage: replay_bundle_capture_sequence --rir <name> --out-dir <path> --tal-path <path> --ta-path <path> [--base-validation-time <rfc3339>] [--delta-count <n>] [--delta-interval-secs <n>] [--http-timeout-secs <n>] [--rsync-timeout-secs <n>] [--rsync-mirror-root <path>] [--max-depth <n>] [--max-instances <n>] [--trust-anchor <name>] [--keep-db] [--capture-inputs-only]"
|
||||
}
|
||||
|
||||
fn parse_args(argv: &[String]) -> Result<Args, String> {
|
||||
let mut args = Args {
|
||||
delta_count: 5,
|
||||
delta_interval_secs: 600,
|
||||
http_timeout_secs: 20,
|
||||
rsync_timeout_secs: 60,
|
||||
..Args::default()
|
||||
};
|
||||
let mut i = 1usize;
|
||||
while i < argv.len() {
|
||||
match argv[i].as_str() {
|
||||
"--help" | "-h" => return Err(usage().to_string()),
|
||||
"--rir" => {
|
||||
i += 1;
|
||||
args.rir = Some(argv.get(i).ok_or("--rir requires a value")?.clone());
|
||||
}
|
||||
"--out-dir" => {
|
||||
i += 1;
|
||||
args.out_dir = Some(PathBuf::from(argv.get(i).ok_or("--out-dir requires a value")?));
|
||||
}
|
||||
"--tal-path" => {
|
||||
i += 1;
|
||||
args.tal_path = Some(PathBuf::from(argv.get(i).ok_or("--tal-path requires a value")?));
|
||||
}
|
||||
"--ta-path" => {
|
||||
i += 1;
|
||||
args.ta_path = Some(PathBuf::from(argv.get(i).ok_or("--ta-path requires a value")?));
|
||||
}
|
||||
"--base-validation-time" => {
|
||||
i += 1;
|
||||
let value = argv.get(i).ok_or("--base-validation-time requires a value")?;
|
||||
args.base_validation_time = Some(
|
||||
time::OffsetDateTime::parse(value, &Rfc3339)
|
||||
.map_err(|e| format!("invalid --base-validation-time: {e}"))?,
|
||||
);
|
||||
}
|
||||
"--delta-count" => {
|
||||
i += 1;
|
||||
args.delta_count = argv
|
||||
.get(i)
|
||||
.ok_or("--delta-count requires a value")?
|
||||
.parse()
|
||||
.map_err(|e| format!("invalid --delta-count: {e}"))?;
|
||||
}
|
||||
"--delta-interval-secs" => {
|
||||
i += 1;
|
||||
args.delta_interval_secs = argv
|
||||
.get(i)
|
||||
.ok_or("--delta-interval-secs requires a value")?
|
||||
.parse()
|
||||
.map_err(|e| format!("invalid --delta-interval-secs: {e}"))?;
|
||||
}
|
||||
"--http-timeout-secs" => {
|
||||
i += 1;
|
||||
args.http_timeout_secs = argv
|
||||
.get(i)
|
||||
.ok_or("--http-timeout-secs requires a value")?
|
||||
.parse()
|
||||
.map_err(|e| format!("invalid --http-timeout-secs: {e}"))?;
|
||||
}
|
||||
"--rsync-timeout-secs" => {
|
||||
i += 1;
|
||||
args.rsync_timeout_secs = argv
|
||||
.get(i)
|
||||
.ok_or("--rsync-timeout-secs requires a value")?
|
||||
.parse()
|
||||
.map_err(|e| format!("invalid --rsync-timeout-secs: {e}"))?;
|
||||
}
|
||||
"--rsync-mirror-root" => {
|
||||
i += 1;
|
||||
args.rsync_mirror_root =
|
||||
Some(PathBuf::from(argv.get(i).ok_or("--rsync-mirror-root requires a value")?));
|
||||
}
|
||||
"--max-depth" => {
|
||||
i += 1;
|
||||
args.max_depth = Some(
|
||||
argv.get(i)
|
||||
.ok_or("--max-depth requires a value")?
|
||||
.parse()
|
||||
.map_err(|e| format!("invalid --max-depth: {e}"))?,
|
||||
);
|
||||
}
|
||||
"--max-instances" => {
|
||||
i += 1;
|
||||
args.max_instances = Some(
|
||||
argv.get(i)
|
||||
.ok_or("--max-instances requires a value")?
|
||||
.parse()
|
||||
.map_err(|e| format!("invalid --max-instances: {e}"))?,
|
||||
);
|
||||
}
|
||||
"--trust-anchor" => {
|
||||
i += 1;
|
||||
args.trust_anchor = Some(argv.get(i).ok_or("--trust-anchor requires a value")?.clone());
|
||||
}
|
||||
"--keep-db" => args.keep_db = true,
|
||||
"--capture-inputs-only" => args.capture_inputs_only = true,
|
||||
other => return Err(format!("unknown argument: {other}\n{}", usage())),
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if args.rir.is_none() {
|
||||
return Err(format!("--rir is required\n{}", usage()));
|
||||
}
|
||||
if args.out_dir.is_none() {
|
||||
return Err(format!("--out-dir is required\n{}", usage()));
|
||||
}
|
||||
if args.tal_path.is_none() {
|
||||
return Err(format!("--tal-path is required\n{}", usage()));
|
||||
}
|
||||
if args.ta_path.is_none() {
|
||||
return Err(format!("--ta-path is required\n{}", usage()));
|
||||
}
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
fn write_v2_top_readme(path: &Path, rir: &str, delta_count: usize, delta_interval_secs: u64) -> Result<(), String> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
|
||||
}
|
||||
fs::write(
|
||||
path,
|
||||
format!(
|
||||
"# Ours Multi-Delta Replay Bundle\n\n- RIR: `{rir}`\n- Schema: `20260401-v2`\n- Configured delta steps: `{delta_count}`\n- Configured interval seconds: `{delta_interval_secs}`\n"
|
||||
),
|
||||
)
|
||||
.map_err(|e| format!("write readme failed: {}: {e}", path.display()))
|
||||
}
|
||||
|
||||
fn write_v2_rir_readme(
|
||||
path: &Path,
|
||||
rir: &str,
|
||||
base_validation_time: &str,
|
||||
delta_count: usize,
|
||||
delta_interval_secs: u64,
|
||||
) -> Result<(), String> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?;
|
||||
}
|
||||
fs::write(
|
||||
path,
|
||||
format!(
|
||||
"# {rir} multi-delta live replay bundle\n\n- `base-locks.json.validationTime` = `{base_validation_time}`\n- `delta-steps/` contains `{delta_count}` ordered target steps\n- configured interval seconds = `{delta_interval_secs}`\n"
|
||||
),
|
||||
)
|
||||
.map_err(|e| format!("write rir readme failed: {}: {e}", path.display()))
|
||||
}
|
||||
|
||||
fn ensure_recorded_target_snapshots_for_locks(
|
||||
store: &RocksStore,
|
||||
previous_locks_path: &Path,
|
||||
http: &RecordingHttpFetcher<BlockingHttpFetcher>,
|
||||
) -> Result<(), String> {
|
||||
let previous_locks: serde_json::Value = serde_json::from_slice(
|
||||
&fs::read(previous_locks_path)
|
||||
.map_err(|e| format!("read previous locks failed: {}: {e}", previous_locks_path.display()))?,
|
||||
)
|
||||
.map_err(|e| format!("parse previous locks failed: {}: {e}", previous_locks_path.display()))?;
|
||||
let previous_rrdp = previous_locks
|
||||
.get("rrdp")
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
for (notify_uri, base_lock) in previous_rrdp {
|
||||
let Some(base_transport) = base_lock.get("transport").and_then(|v| v.as_str()) else {
|
||||
continue;
|
||||
};
|
||||
if base_transport != "rrdp" {
|
||||
continue;
|
||||
}
|
||||
let Some(base_session) = base_lock.get("session").and_then(|v| v.as_str()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(base_serial) = base_lock.get("serial").and_then(|v| v.as_u64()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(record) = store
|
||||
.get_rrdp_source_record(¬ify_uri)
|
||||
.map_err(|e| format!("read rrdp source record failed for {notify_uri}: {e}"))?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(target_session) = record.last_session_id.as_deref() else {
|
||||
continue;
|
||||
};
|
||||
let Some(target_serial) = record.last_serial else {
|
||||
continue;
|
||||
};
|
||||
if target_session != base_session || target_serial <= base_serial {
|
||||
continue;
|
||||
}
|
||||
let Some(snapshot_uri) = record.last_snapshot_uri.as_deref() else {
|
||||
continue;
|
||||
};
|
||||
if step_http_has_snapshot(http, snapshot_uri) {
|
||||
continue;
|
||||
}
|
||||
if let Err(err) = http.fetch(snapshot_uri) {
|
||||
eprintln!(
|
||||
"[sequence] warning: fetch target snapshot failed notify_uri={} snapshot_uri={} err={}",
|
||||
notify_uri,
|
||||
snapshot_uri,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn step_http_has_snapshot(
|
||||
http: &RecordingHttpFetcher<BlockingHttpFetcher>,
|
||||
snapshot_uri: &str,
|
||||
) -> bool {
|
||||
http.snapshot_responses().contains_key(snapshot_uri)
|
||||
}
|
||||
|
||||
fn run(args: Args) -> Result<PathBuf, String> {
|
||||
let rir = args.rir.as_ref().unwrap();
|
||||
let rir_normalized = rir.to_ascii_lowercase();
|
||||
let trust_anchor = args
|
||||
.trust_anchor
|
||||
.clone()
|
||||
.unwrap_or_else(|| rir_normalized.clone());
|
||||
let out_root = args.out_dir.as_ref().unwrap();
|
||||
if out_root.exists() {
|
||||
fs::remove_dir_all(out_root)
|
||||
.map_err(|e| format!("remove old out dir failed: {}: {e}", out_root.display()))?;
|
||||
}
|
||||
let rir_dir = out_root.join(&rir_normalized);
|
||||
let delta_steps_root = rir_dir.join("delta-steps");
|
||||
fs::create_dir_all(&delta_steps_root)
|
||||
.map_err(|e| format!("create delta steps dir failed: {}: {e}", delta_steps_root.display()))?;
|
||||
|
||||
let tal_bytes = fs::read(args.tal_path.as_ref().unwrap())
|
||||
.map_err(|e| format!("read tal failed: {e}"))?;
|
||||
let ta_bytes = fs::read(args.ta_path.as_ref().unwrap())
|
||||
.map_err(|e| format!("read ta failed: {e}"))?;
|
||||
fs::write(rir_dir.join("tal.tal"), &tal_bytes).map_err(|e| format!("write tal failed: {e}"))?;
|
||||
fs::write(rir_dir.join("ta.cer"), &ta_bytes).map_err(|e| format!("write ta failed: {e}"))?;
|
||||
|
||||
let base_validation_time = args.base_validation_time.unwrap_or_else(time::OffsetDateTime::now_utc);
|
||||
|
||||
let work_db_dir = out_root.join(".tmp").join(format!("{rir}-sequence-work-db"));
|
||||
let base_self_replay_dir = out_root.join(".tmp").join(format!("{rir}-sequence-base-self-replay-db"));
|
||||
let _ = fs::remove_dir_all(&work_db_dir);
|
||||
let _ = fs::remove_dir_all(&base_self_replay_dir);
|
||||
if let Some(parent) = work_db_dir.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("create tmp dir failed: {}: {e}", parent.display()))?;
|
||||
}
|
||||
|
||||
let store = RocksStore::open(&work_db_dir).map_err(|e| format!("open work db failed: {e}"))?;
|
||||
let base_http = RecordingHttpFetcher::new(
|
||||
BlockingHttpFetcher::new(HttpFetcherConfig {
|
||||
timeout: Duration::from_secs(args.http_timeout_secs),
|
||||
..HttpFetcherConfig::default()
|
||||
})
|
||||
.map_err(|e| format!("create base http fetcher failed: {e}"))?,
|
||||
);
|
||||
let base_rsync_inner = SystemRsyncFetcher::new(SystemRsyncConfig {
|
||||
timeout: Duration::from_secs(args.rsync_timeout_secs),
|
||||
mirror_root: args.rsync_mirror_root.clone(),
|
||||
..SystemRsyncConfig::default()
|
||||
});
|
||||
let base_rsync = if args.capture_inputs_only {
|
||||
RecordingRsyncFetcher::new_without_objects(base_rsync_inner)
|
||||
} else {
|
||||
RecordingRsyncFetcher::new(base_rsync_inner)
|
||||
};
|
||||
eprintln!("[sequence] base live run start rir={rir_normalized}");
|
||||
let started = Instant::now();
|
||||
let base_out = run_tree_from_tal_and_ta_der_serial_audit(
|
||||
&store,
|
||||
&Policy::default(),
|
||||
&tal_bytes,
|
||||
&ta_bytes,
|
||||
None,
|
||||
&base_http,
|
||||
&base_rsync,
|
||||
base_validation_time,
|
||||
&TreeRunConfig {
|
||||
max_depth: args.max_depth,
|
||||
max_instances: args.max_instances,
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("live base run failed: {e}"))?;
|
||||
let base_duration = started.elapsed();
|
||||
eprintln!(
|
||||
"[sequence] base live run done rir={} duration_s={:.3}",
|
||||
rir_normalized,
|
||||
base_duration.as_secs_f64()
|
||||
);
|
||||
|
||||
eprintln!("[sequence] base input materialization start rir={rir_normalized}");
|
||||
let base_capture = write_live_base_replay_bundle_inputs(
|
||||
&rir_dir,
|
||||
&rir_normalized,
|
||||
base_validation_time,
|
||||
&base_out.publication_points,
|
||||
&store,
|
||||
&base_http.snapshot_responses(),
|
||||
&base_rsync.snapshot_fetches(),
|
||||
)?;
|
||||
eprintln!(
|
||||
"[sequence] base input materialization done rir={} rrdp_repos={} rsync_modules={}",
|
||||
rir_normalized,
|
||||
base_capture.rrdp_repo_count,
|
||||
base_capture.rsync_module_count
|
||||
);
|
||||
let base_ccr_path = rir_dir.join("base.ccr");
|
||||
let base_vrps_path = rir_dir.join("base-vrps.csv");
|
||||
let base_vaps_path = rir_dir.join("base-vaps.csv");
|
||||
let (base_ccr_sha256, base_vrp_rows, base_vap_rows, base_verify, base_self_replay_ok) =
|
||||
if args.capture_inputs_only {
|
||||
eprintln!("[sequence] base output generation skipped rir={rir_normalized}");
|
||||
(
|
||||
String::new(),
|
||||
std::collections::BTreeSet::<rpki::bundle::VrpCompareRow>::new(),
|
||||
std::collections::BTreeSet::<rpki::bundle::VapCompareRow>::new(),
|
||||
CcrVerifySummary {
|
||||
content_type_oid: String::new(),
|
||||
version: 0,
|
||||
produced_at_rfc3339_utc: String::new(),
|
||||
state_hashes_ok: false,
|
||||
manifest_instances: 0,
|
||||
roa_payload_sets: 0,
|
||||
roa_vrp_count: 0,
|
||||
aspa_payload_sets: 0,
|
||||
trust_anchor_ski_count: 0,
|
||||
router_key_sets: 0,
|
||||
router_key_count: 0,
|
||||
},
|
||||
false,
|
||||
)
|
||||
} else {
|
||||
eprintln!("[sequence] base CCR/self-replay start rir={rir_normalized}");
|
||||
let base_ccr = build_ccr_from_run(
|
||||
&store,
|
||||
&[base_out.discovery.trust_anchor.clone()],
|
||||
&base_out.tree.vrps,
|
||||
&base_out.tree.aspas,
|
||||
&base_out.tree.router_keys,
|
||||
base_validation_time,
|
||||
)
|
||||
.map_err(|e| format!("build base ccr failed: {e}"))?;
|
||||
write_ccr_file(&base_ccr_path, &base_ccr)
|
||||
.map_err(|e| format!("write base ccr failed: {e}"))?;
|
||||
let base_ccr_bytes = fs::read(&base_ccr_path)
|
||||
.map_err(|e| format!("read base ccr failed: {}: {e}", base_ccr_path.display()))?;
|
||||
let base_decoded = decode_content_info(&base_ccr_bytes)
|
||||
.map_err(|e| format!("decode base ccr failed: {e}"))?;
|
||||
let base_verify =
|
||||
verify_content_info(&base_decoded).map_err(|e| format!("verify base ccr failed: {e}"))?;
|
||||
let base_vrp_rows = build_vrp_compare_rows(&base_out.tree.vrps, &trust_anchor);
|
||||
let base_vap_rows = build_vap_compare_rows(&base_out.tree.aspas, &trust_anchor);
|
||||
let (base_ccr_vrps, base_ccr_vaps) =
|
||||
rpki::bundle::decode_ccr_compare_views(&base_decoded, &trust_anchor)?;
|
||||
if base_vrp_rows != base_ccr_vrps {
|
||||
return Err("base-vrps compare view does not match base.ccr".to_string());
|
||||
}
|
||||
if base_vap_rows != base_ccr_vaps {
|
||||
return Err("base-vaps compare view does not match base.ccr".to_string());
|
||||
}
|
||||
write_vrp_csv(&base_vrps_path, &base_vrp_rows)?;
|
||||
write_vap_csv(&base_vaps_path, &base_vap_rows)?;
|
||||
let base_replay_store = RocksStore::open(&base_self_replay_dir)
|
||||
.map_err(|e| format!("open base self replay db failed: {e}"))?;
|
||||
let base_replay_out = run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
|
||||
&base_replay_store,
|
||||
&Policy::default(),
|
||||
&tal_bytes,
|
||||
&ta_bytes,
|
||||
None,
|
||||
&rir_dir.join("base-payload-archive"),
|
||||
&rir_dir.join("base-locks.json"),
|
||||
base_validation_time,
|
||||
&TreeRunConfig {
|
||||
max_depth: args.max_depth,
|
||||
max_instances: args.max_instances,
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("base self replay failed: {e}"))?;
|
||||
if build_vrp_compare_rows(&base_replay_out.tree.vrps, &trust_anchor) != base_vrp_rows {
|
||||
return Err("base self replay VRP compare view mismatch".to_string());
|
||||
}
|
||||
if build_vap_compare_rows(&base_replay_out.tree.aspas, &trust_anchor) != base_vap_rows {
|
||||
return Err("base self replay VAP compare view mismatch".to_string());
|
||||
}
|
||||
let output = (
|
||||
sha256_hex(&base_ccr_bytes),
|
||||
base_vrp_rows,
|
||||
base_vap_rows,
|
||||
base_verify,
|
||||
true,
|
||||
);
|
||||
eprintln!("[sequence] base CCR/self-replay done rir={rir_normalized}");
|
||||
output
|
||||
};
|
||||
fs::create_dir_all(rir_dir.join("timings"))
|
||||
.map_err(|e| format!("create timings dir failed: {e}"))?;
|
||||
write_json(
|
||||
&rir_dir.join("timings").join("base-produce.json"),
|
||||
&serde_json::json!({
|
||||
"mode": "base",
|
||||
"validationTime": base_validation_time.format(&Rfc3339).map_err(|e| format!("format base validation time failed: {e}"))?,
|
||||
"durationSeconds": base_duration.as_secs_f64(),
|
||||
}),
|
||||
)?;
|
||||
|
||||
let mut steps_json = Vec::new();
|
||||
let mut delta_steps = Vec::new();
|
||||
let mut previous_locks_path = rir_dir.join("base-locks.json");
|
||||
let mut previous_ref = "base".to_string();
|
||||
let sequence_self_replay_dir = out_root.join(".tmp").join(format!("{rir}-sequence-self-replay-db"));
|
||||
let _ = fs::remove_dir_all(&sequence_self_replay_dir);
|
||||
let sequence_replay_store = if args.capture_inputs_only {
|
||||
None
|
||||
} else {
|
||||
let store = RocksStore::open(&sequence_self_replay_dir)
|
||||
.map_err(|e| format!("open sequence self replay db failed: {e}"))?;
|
||||
let _base_replay = run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
|
||||
&store,
|
||||
&Policy::default(),
|
||||
&tal_bytes,
|
||||
&ta_bytes,
|
||||
None,
|
||||
&rir_dir.join("base-payload-archive"),
|
||||
&rir_dir.join("base-locks.json"),
|
||||
base_validation_time,
|
||||
&TreeRunConfig {
|
||||
max_depth: args.max_depth,
|
||||
max_instances: args.max_instances,
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("sequence base self replay failed: {e}"))?;
|
||||
Some(store)
|
||||
};
|
||||
let mut all_steps_self_replay_ok = true;
|
||||
|
||||
for step_index in 1..=args.delta_count {
|
||||
if step_index > 1 && args.delta_interval_secs > 0 {
|
||||
std::thread::sleep(Duration::from_secs(args.delta_interval_secs));
|
||||
}
|
||||
let step_id = format!("step-{step_index:03}");
|
||||
let step_dir = delta_steps_root.join(&step_id);
|
||||
fs::create_dir_all(&step_dir)
|
||||
.map_err(|e| format!("create step dir failed: {}: {e}", step_dir.display()))?;
|
||||
let step_validation_time = time::OffsetDateTime::now_utc();
|
||||
eprintln!("[sequence] step live run start rir={} step={}", rir_normalized, step_id);
|
||||
let step_http = RecordingHttpFetcher::new(
|
||||
BlockingHttpFetcher::new(HttpFetcherConfig {
|
||||
timeout: Duration::from_secs(args.http_timeout_secs),
|
||||
..HttpFetcherConfig::default()
|
||||
})
|
||||
.map_err(|e| format!("create step http fetcher failed: {e}"))?,
|
||||
);
|
||||
let step_rsync_inner = SystemRsyncFetcher::new(SystemRsyncConfig {
|
||||
timeout: Duration::from_secs(args.rsync_timeout_secs),
|
||||
mirror_root: args.rsync_mirror_root.clone(),
|
||||
..SystemRsyncConfig::default()
|
||||
});
|
||||
let step_rsync = if args.capture_inputs_only {
|
||||
RecordingRsyncFetcher::new_without_objects(step_rsync_inner)
|
||||
} else {
|
||||
RecordingRsyncFetcher::new(step_rsync_inner)
|
||||
};
|
||||
let started = Instant::now();
|
||||
let step_out = run_tree_from_tal_and_ta_der_serial_audit(
|
||||
&store,
|
||||
&Policy::default(),
|
||||
&tal_bytes,
|
||||
&ta_bytes,
|
||||
None,
|
||||
&step_http,
|
||||
&step_rsync,
|
||||
step_validation_time,
|
||||
&TreeRunConfig {
|
||||
max_depth: args.max_depth,
|
||||
max_instances: args.max_instances,
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("live delta step {step_id} failed: {e}"))?;
|
||||
let step_duration = started.elapsed();
|
||||
eprintln!(
|
||||
"[sequence] step live run done rir={} step={} duration_s={:.3}",
|
||||
rir_normalized,
|
||||
step_id,
|
||||
step_duration.as_secs_f64()
|
||||
);
|
||||
ensure_recorded_target_snapshots_for_locks(&store, &previous_locks_path, &step_http)?;
|
||||
|
||||
eprintln!("[sequence] step output generation phase start rir={} step={}", rir_normalized, step_id);
|
||||
let delta_ccr_path = step_dir.join("delta.ccr");
|
||||
let delta_vrps_path = step_dir.join("record-delta.csv");
|
||||
let delta_vaps_path = step_dir.join("record-delta-vaps.csv");
|
||||
let (delta_ccr_sha256, delta_vrp_rows, delta_vap_rows, delta_verify, step_self_replay_ok) =
|
||||
if args.capture_inputs_only {
|
||||
eprintln!(
|
||||
"[sequence] step CCR/self-replay skipped rir={} step={}",
|
||||
rir_normalized,
|
||||
step_id
|
||||
);
|
||||
(
|
||||
String::new(),
|
||||
std::collections::BTreeSet::<rpki::bundle::VrpCompareRow>::new(),
|
||||
std::collections::BTreeSet::<rpki::bundle::VapCompareRow>::new(),
|
||||
CcrVerifySummary {
|
||||
content_type_oid: String::new(),
|
||||
version: 0,
|
||||
produced_at_rfc3339_utc: String::new(),
|
||||
state_hashes_ok: false,
|
||||
manifest_instances: 0,
|
||||
roa_payload_sets: 0,
|
||||
roa_vrp_count: 0,
|
||||
aspa_payload_sets: 0,
|
||||
trust_anchor_ski_count: 0,
|
||||
router_key_sets: 0,
|
||||
router_key_count: 0,
|
||||
},
|
||||
false,
|
||||
)
|
||||
} else {
|
||||
eprintln!("[sequence] step CCR/self-replay start rir={} step={}", rir_normalized, step_id);
|
||||
let delta_ccr = build_ccr_from_run(
|
||||
&store,
|
||||
&[step_out.discovery.trust_anchor.clone()],
|
||||
&step_out.tree.vrps,
|
||||
&step_out.tree.aspas,
|
||||
&step_out.tree.router_keys,
|
||||
step_validation_time,
|
||||
)
|
||||
.map_err(|e| format!("build step ccr failed for {step_id}: {e}"))?;
|
||||
write_ccr_file(&delta_ccr_path, &delta_ccr)
|
||||
.map_err(|e| format!("write step ccr failed for {step_id}: {e}"))?;
|
||||
let delta_ccr_bytes = fs::read(&delta_ccr_path).map_err(|e| {
|
||||
format!("read step ccr failed: {}: {e}", delta_ccr_path.display())
|
||||
})?;
|
||||
let delta_decoded = decode_content_info(&delta_ccr_bytes)
|
||||
.map_err(|e| format!("decode step ccr failed for {step_id}: {e}"))?;
|
||||
let delta_verify = verify_content_info(&delta_decoded)
|
||||
.map_err(|e| format!("verify step ccr failed for {step_id}: {e}"))?;
|
||||
let delta_vrp_rows = build_vrp_compare_rows(&step_out.tree.vrps, &trust_anchor);
|
||||
let delta_vap_rows = build_vap_compare_rows(&step_out.tree.aspas, &trust_anchor);
|
||||
let (ccr_vrps, ccr_vaps) =
|
||||
rpki::bundle::decode_ccr_compare_views(&delta_decoded, &trust_anchor)?;
|
||||
if delta_vrp_rows != ccr_vrps {
|
||||
return Err(format!("{step_id} VRP compare view does not match delta.ccr"));
|
||||
}
|
||||
if delta_vap_rows != ccr_vaps {
|
||||
return Err(format!("{step_id} VAP compare view does not match delta.ccr"));
|
||||
}
|
||||
write_vrp_csv(&delta_vrps_path, &delta_vrp_rows)?;
|
||||
write_vap_csv(&delta_vaps_path, &delta_vap_rows)?;
|
||||
let step_replay_out = run_tree_from_tal_and_ta_der_payload_delta_replay_step_serial_audit(
|
||||
sequence_replay_store.as_ref().expect("sequence replay store"),
|
||||
&Policy::default(),
|
||||
&tal_bytes,
|
||||
&ta_bytes,
|
||||
None,
|
||||
&step_dir.join("payload-delta-archive"),
|
||||
&previous_locks_path,
|
||||
&step_dir.join("locks-delta.json"),
|
||||
step_validation_time,
|
||||
&TreeRunConfig {
|
||||
max_depth: args.max_depth,
|
||||
max_instances: args.max_instances,
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("sequence self replay failed for {step_id}: {e}"))?;
|
||||
let step_self_replay_ok =
|
||||
build_vrp_compare_rows(&step_replay_out.tree.vrps, &trust_anchor) == delta_vrp_rows
|
||||
&& build_vap_compare_rows(&step_replay_out.tree.aspas, &trust_anchor)
|
||||
== delta_vap_rows;
|
||||
let output = (
|
||||
sha256_hex(&delta_ccr_bytes),
|
||||
delta_vrp_rows,
|
||||
delta_vap_rows,
|
||||
delta_verify,
|
||||
step_self_replay_ok,
|
||||
);
|
||||
eprintln!("[sequence] step CCR/self-replay done rir={} step={}", rir_normalized, step_id);
|
||||
output
|
||||
};
|
||||
|
||||
eprintln!("[sequence] step input materialization start rir={} step={}", rir_normalized, step_id);
|
||||
let delta_capture = write_live_delta_replay_step_inputs(
|
||||
&step_dir,
|
||||
&rir_normalized,
|
||||
&previous_locks_path,
|
||||
step_validation_time,
|
||||
&step_out.publication_points,
|
||||
&store,
|
||||
&step_http.snapshot_responses(),
|
||||
&step_rsync.snapshot_fetches(),
|
||||
)?;
|
||||
let target_lock_capture_id = format!("{rir_normalized}-target-{step_id}");
|
||||
write_current_replay_state_locks(
|
||||
&step_dir.join("target-locks.json"),
|
||||
&target_lock_capture_id,
|
||||
step_validation_time,
|
||||
&step_out.publication_points,
|
||||
&store,
|
||||
)?;
|
||||
eprintln!(
|
||||
"[sequence] step input materialization done rir={} step={} rrdp_repos={} rsync_modules={}",
|
||||
rir_normalized,
|
||||
step_id,
|
||||
delta_capture.rrdp_repo_count,
|
||||
delta_capture.rsync_module_count
|
||||
);
|
||||
write_json(
|
||||
&step_dir.join("verification.json"),
|
||||
&serde_json::json!({
|
||||
"index": step_index,
|
||||
"id": step_id,
|
||||
"validationTime": step_validation_time.format(&Rfc3339).map_err(|e| format!("format validation time failed: {e}"))?,
|
||||
"capture": {
|
||||
"captureId": delta_capture.capture_id,
|
||||
"rrdpRepoCount": delta_capture.rrdp_repo_count,
|
||||
"rsyncModuleCount": delta_capture.rsync_module_count,
|
||||
},
|
||||
"ccr": {
|
||||
"path": "delta.ccr",
|
||||
"sha256": delta_ccr_sha256.clone(),
|
||||
"stateHashesOk": delta_verify.state_hashes_ok,
|
||||
"manifestInstances": delta_verify.manifest_instances,
|
||||
"roaVrpCount": delta_verify.roa_vrp_count,
|
||||
"aspaPayloadSets": delta_verify.aspa_payload_sets,
|
||||
"routerKeyCount": delta_verify.router_key_count,
|
||||
},
|
||||
"compareViews": {
|
||||
"vrpCount": delta_vrp_rows.len(),
|
||||
"vapCount": delta_vap_rows.len(),
|
||||
},
|
||||
"selfReplayOk": serde_json::Value::Null,
|
||||
"timings": {
|
||||
"durationSeconds": step_duration.as_secs_f64(),
|
||||
}
|
||||
}),
|
||||
)?;
|
||||
fs::create_dir_all(step_dir.join("timings"))
|
||||
.map_err(|e| format!("create step timings dir failed: {e}"))?;
|
||||
write_json(
|
||||
&step_dir.join("timings").join("delta-produce.json"),
|
||||
&serde_json::json!({
|
||||
"mode": "delta",
|
||||
"stepIndex": step_index,
|
||||
"validationTime": step_validation_time.format(&Rfc3339).map_err(|e| format!("format validation time failed: {e}"))?,
|
||||
"durationSeconds": step_duration.as_secs_f64(),
|
||||
}),
|
||||
)?;
|
||||
|
||||
delta_steps.push(DeltaStepMetadataV2 {
|
||||
index: step_index,
|
||||
id: step_id.clone(),
|
||||
relative_path: format!("delta-steps/{step_id}"),
|
||||
base_ref: previous_ref.clone(),
|
||||
validation_time: step_validation_time
|
||||
.format(&Rfc3339)
|
||||
.map_err(|e| format!("format validation time failed: {e}"))?,
|
||||
delta_ccr_sha256: delta_ccr_sha256.clone(),
|
||||
vrp_count: delta_vrp_rows.len(),
|
||||
vap_count: delta_vap_rows.len(),
|
||||
relative_archive_path: format!("delta-steps/{step_id}/payload-delta-archive"),
|
||||
relative_transition_locks_path: format!("delta-steps/{step_id}/locks-delta.json"),
|
||||
relative_target_locks_path: format!("delta-steps/{step_id}/target-locks.json"),
|
||||
relative_ccr_path: format!("delta-steps/{step_id}/delta.ccr"),
|
||||
relative_vrps_path: format!("delta-steps/{step_id}/record-delta.csv"),
|
||||
relative_vaps_path: format!("delta-steps/{step_id}/record-delta-vaps.csv"),
|
||||
has_aspa: !delta_vap_rows.is_empty(),
|
||||
has_router_key: delta_verify.router_key_count > 0,
|
||||
});
|
||||
all_steps_self_replay_ok &= step_self_replay_ok;
|
||||
|
||||
steps_json.push(serde_json::json!({
|
||||
"index": step_index,
|
||||
"id": step_id,
|
||||
"validationTime": step_validation_time.format(&Rfc3339).map_err(|e| format!("format validation time failed: {e}"))?,
|
||||
"capture": {
|
||||
"captureId": delta_capture.capture_id,
|
||||
"rrdpRepoCount": delta_capture.rrdp_repo_count,
|
||||
"rsyncModuleCount": delta_capture.rsync_module_count,
|
||||
},
|
||||
"ccr": {
|
||||
"path": format!("delta-steps/{step_id}/delta.ccr"),
|
||||
"sha256": delta_ccr_sha256,
|
||||
"stateHashesOk": delta_verify.state_hashes_ok,
|
||||
"manifestInstances": delta_verify.manifest_instances,
|
||||
"roaVrpCount": delta_verify.roa_vrp_count,
|
||||
"aspaPayloadSets": delta_verify.aspa_payload_sets,
|
||||
"routerKeyCount": delta_verify.router_key_count,
|
||||
},
|
||||
"compareViews": {
|
||||
"vrpCount": delta_vrp_rows.len(),
|
||||
"vapCount": delta_vap_rows.len(),
|
||||
},
|
||||
"selfReplayOk": if args.capture_inputs_only { serde_json::Value::Null } else { serde_json::Value::Bool(step_self_replay_ok) }
|
||||
}));
|
||||
|
||||
previous_locks_path = step_dir.join("target-locks.json");
|
||||
previous_ref = step_id;
|
||||
}
|
||||
|
||||
let metadata = RirBundleMetadataV2 {
|
||||
schema_version: "20260401-v2".to_string(),
|
||||
bundle_producer: "ours".to_string(),
|
||||
rir: rir_normalized.clone(),
|
||||
tal_sha256: sha256_hex(&tal_bytes),
|
||||
ta_cert_sha256: sha256_hex(&ta_bytes),
|
||||
has_any_aspa: !base_vap_rows.is_empty() || delta_steps.iter().any(|step| step.has_aspa),
|
||||
has_any_router_key: base_verify.router_key_count > 0
|
||||
|| delta_steps.iter().any(|step| step.has_router_key),
|
||||
base: BaseBundleStateMetadataV2 {
|
||||
validation_time: base_validation_time
|
||||
.format(&Rfc3339)
|
||||
.map_err(|e| format!("format base validation time failed: {e}"))?,
|
||||
ccr_sha256: base_ccr_sha256.clone(),
|
||||
vrp_count: base_vrp_rows.len(),
|
||||
vap_count: base_vap_rows.len(),
|
||||
relative_archive_path: "base-payload-archive".to_string(),
|
||||
relative_locks_path: "base-locks.json".to_string(),
|
||||
relative_ccr_path: "base.ccr".to_string(),
|
||||
relative_vrps_path: "base-vrps.csv".to_string(),
|
||||
relative_vaps_path: "base-vaps.csv".to_string(),
|
||||
},
|
||||
delta_sequence: DeltaSequenceMetadataV2 {
|
||||
configured_delta_count: args.delta_count,
|
||||
configured_interval_seconds: args.delta_interval_secs,
|
||||
steps: delta_steps.clone(),
|
||||
},
|
||||
};
|
||||
write_json(&rir_dir.join("bundle.json"), &metadata)?;
|
||||
write_json(
|
||||
&rir_dir.join("verification.json"),
|
||||
&serde_json::json!({
|
||||
"base": {
|
||||
"validationTime": metadata.base.validation_time,
|
||||
"ccr": {
|
||||
"path": "base.ccr",
|
||||
"sha256": metadata.base.ccr_sha256,
|
||||
"stateHashesOk": base_verify.state_hashes_ok,
|
||||
"manifestInstances": base_verify.manifest_instances,
|
||||
"roaVrpCount": base_verify.roa_vrp_count,
|
||||
"aspaPayloadSets": base_verify.aspa_payload_sets,
|
||||
"routerKeyCount": base_verify.router_key_count,
|
||||
},
|
||||
"compareViews": {
|
||||
"baseVrpCount": metadata.base.vrp_count,
|
||||
"baseVapCount": metadata.base.vap_count,
|
||||
},
|
||||
"capture": {
|
||||
"captureId": base_capture.capture_id,
|
||||
"rrdpRepoCount": base_capture.rrdp_repo_count,
|
||||
"rsyncModuleCount": base_capture.rsync_module_count,
|
||||
"selfReplayOk": if args.capture_inputs_only { serde_json::Value::Null } else { serde_json::Value::Bool(base_self_replay_ok) },
|
||||
}
|
||||
},
|
||||
"steps": steps_json,
|
||||
"summary": {
|
||||
"baseSelfReplayOk": if args.capture_inputs_only { serde_json::Value::Null } else { serde_json::Value::Bool(base_self_replay_ok) },
|
||||
"stepCount": args.delta_count,
|
||||
"allStepsSelfReplayOk": if args.capture_inputs_only { serde_json::Value::Null } else { serde_json::Value::Bool(all_steps_self_replay_ok) },
|
||||
}
|
||||
}),
|
||||
)?;
|
||||
write_v2_top_readme(&out_root.join("README.md"), &rir_normalized, args.delta_count, args.delta_interval_secs)?;
|
||||
write_v2_rir_readme(
|
||||
&rir_dir.join("README.md"),
|
||||
&rir_normalized,
|
||||
&metadata.base.validation_time,
|
||||
args.delta_count,
|
||||
args.delta_interval_secs,
|
||||
)?;
|
||||
|
||||
let bundle_manifest = BundleManifestV2 {
|
||||
schema_version: "20260401-v2".to_string(),
|
||||
bundle_producer: "ours".to_string(),
|
||||
recorded_at_rfc3339_utc: time::OffsetDateTime::now_utc()
|
||||
.format(&Rfc3339)
|
||||
.map_err(|e| format!("format recorded_at failed: {e}"))?,
|
||||
rirs: vec![rir_normalized.clone()],
|
||||
per_rir_bundles: vec![BundleManifestEntryV2 {
|
||||
rir: rir_normalized.clone(),
|
||||
relative_path: rir_normalized,
|
||||
base_validation_time: metadata.base.validation_time.clone(),
|
||||
step_count: metadata.delta_sequence.steps.len(),
|
||||
first_delta_validation_time: metadata
|
||||
.delta_sequence
|
||||
.steps
|
||||
.first()
|
||||
.map(|step| step.validation_time.clone()),
|
||||
last_delta_validation_time: metadata
|
||||
.delta_sequence
|
||||
.steps
|
||||
.last()
|
||||
.map(|step| step.validation_time.clone()),
|
||||
has_aspa: metadata.has_any_aspa,
|
||||
}],
|
||||
};
|
||||
write_json(&out_root.join("bundle-manifest.json"), &bundle_manifest)?;
|
||||
|
||||
if !args.keep_db {
|
||||
drop(sequence_replay_store);
|
||||
drop(store);
|
||||
let _ = fs::remove_dir_all(&work_db_dir);
|
||||
let _ = fs::remove_dir_all(&base_self_replay_dir);
|
||||
let _ = fs::remove_dir_all(&sequence_self_replay_dir);
|
||||
let tmp_dir = out_root.join(".tmp");
|
||||
if tmp_dir.is_dir() {
|
||||
let is_empty = fs::read_dir(&tmp_dir)
|
||||
.map_err(|e| format!("read tmp dir failed: {}: {e}", tmp_dir.display()))?
|
||||
.next()
|
||||
.is_none();
|
||||
if is_empty {
|
||||
let _ = fs::remove_dir(&tmp_dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out_root.clone())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), String> {
|
||||
let args = parse_args(&std::env::args().collect::<Vec<_>>())?;
|
||||
let out = run(args)?;
|
||||
println!("{}", out.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_args_defaults_delta_sequence_parameters() {
|
||||
let argv = vec![
|
||||
"replay_bundle_capture_sequence".to_string(),
|
||||
"--rir".to_string(),
|
||||
"apnic".to_string(),
|
||||
"--out-dir".to_string(),
|
||||
"out".to_string(),
|
||||
"--tal-path".to_string(),
|
||||
"tal".to_string(),
|
||||
"--ta-path".to_string(),
|
||||
"ta".to_string(),
|
||||
];
|
||||
let args = parse_args(&argv).expect("parse");
|
||||
assert_eq!(args.delta_count, 5);
|
||||
assert_eq!(args.delta_interval_secs, 600);
|
||||
assert!(!args.keep_db);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_accepts_overrides_and_keep_db() {
|
||||
let argv = vec![
|
||||
"replay_bundle_capture_sequence".to_string(),
|
||||
"--rir".to_string(),
|
||||
"apnic".to_string(),
|
||||
"--out-dir".to_string(),
|
||||
"out".to_string(),
|
||||
"--tal-path".to_string(),
|
||||
"tal".to_string(),
|
||||
"--ta-path".to_string(),
|
||||
"ta".to_string(),
|
||||
"--delta-count".to_string(),
|
||||
"2".to_string(),
|
||||
"--delta-interval-secs".to_string(),
|
||||
"0".to_string(),
|
||||
"--keep-db".to_string(),
|
||||
];
|
||||
let args = parse_args(&argv).expect("parse");
|
||||
assert_eq!(args.delta_count, 2);
|
||||
assert_eq!(args.delta_interval_secs, 0);
|
||||
assert!(args.keep_db);
|
||||
}
|
||||
}
|
||||
1009
src/bin/replay_bundle_refresh_sequence_outputs.rs
Normal file
1009
src/bin/replay_bundle_refresh_sequence_outputs.rs
Normal file
File diff suppressed because it is too large
Load Diff
100
src/bin/trace_arin_missing_vrps.rs
Normal file
100
src/bin/trace_arin_missing_vrps.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use rpki::audit_trace::trace_rule_to_root;
|
||||
use rpki::storage::{AuditRuleKind, RocksStore, VcirOutputType};
|
||||
use serde_json::Value;
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() < 3 {
|
||||
eprintln!("usage: trace_arin_missing_vrps <db> <row> [<row> ...]");
|
||||
std::process::exit(2);
|
||||
}
|
||||
|
||||
let store = RocksStore::open(Path::new(&args[1])).expect("open db");
|
||||
let vcirs = store.list_vcirs().expect("list vcirs");
|
||||
|
||||
for row in &args[2..] {
|
||||
let parts: Vec<&str> = row.split(',').collect();
|
||||
if parts.len() != 4 {
|
||||
println!("ROW {row}");
|
||||
println!("ERROR invalid compare row");
|
||||
println!();
|
||||
continue;
|
||||
}
|
||||
let asn: u32 = parts[0]
|
||||
.trim_start_matches("AS")
|
||||
.parse()
|
||||
.expect("parse asn");
|
||||
let prefix = parts[1].to_string();
|
||||
let max_length: u8 = parts[2].parse().expect("parse max length");
|
||||
let mut found = false;
|
||||
|
||||
println!("ROW {row}");
|
||||
for vcir in &vcirs {
|
||||
for output in &vcir.local_outputs {
|
||||
if output.output_type != VcirOutputType::Vrp {
|
||||
continue;
|
||||
}
|
||||
let payload: Value = match serde_json::from_str(&output.payload_json) {
|
||||
Ok(value) => value,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let payload_asn = payload
|
||||
.get("asn")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|v| v as u32);
|
||||
let payload_prefix = payload
|
||||
.get("prefix")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|v| v.to_string());
|
||||
let payload_max = payload
|
||||
.get("max_length")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|v| v as u8);
|
||||
if payload_asn == Some(asn)
|
||||
&& payload_prefix.as_ref() == Some(&prefix)
|
||||
&& payload_max == Some(max_length)
|
||||
{
|
||||
found = true;
|
||||
println!("manifest_rsync_uri={}", vcir.manifest_rsync_uri);
|
||||
println!("source_object_uri={}", output.source_object_uri);
|
||||
println!("source_object_hash={}", output.source_object_hash);
|
||||
println!("source_ee_cert_hash={}", output.source_ee_cert_hash);
|
||||
println!("rule_hash={}", output.rule_hash);
|
||||
println!("validation_path_hint={:?}", output.validation_path_hint);
|
||||
|
||||
if let Some(trace) = trace_rule_to_root(
|
||||
&store,
|
||||
AuditRuleKind::Roa,
|
||||
&output.rule_hash,
|
||||
)
|
||||
.expect("trace rule")
|
||||
{
|
||||
println!(
|
||||
"trace_leaf_manifest={}",
|
||||
trace
|
||||
.chain_leaf_to_root
|
||||
.first()
|
||||
.map(|node| node.manifest_rsync_uri.as_str())
|
||||
.unwrap_or("")
|
||||
);
|
||||
println!("trace_source_object_uri={}", trace.resolved_output.source_object_uri);
|
||||
println!("trace_chain_len={}", trace.chain_leaf_to_root.len());
|
||||
for (idx, node) in trace.chain_leaf_to_root.iter().enumerate() {
|
||||
println!("chain[{idx}].manifest={}", node.manifest_rsync_uri);
|
||||
println!("chain[{idx}].current_manifest={}", node.current_manifest_rsync_uri);
|
||||
println!("chain[{idx}].current_crl={}", node.current_crl_rsync_uri);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
println!("NOT_FOUND");
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user