初始化 Panda RPKI v0.1.0 开源候选版本
This commit is contained in:
548
src/runtime/run.rs
Normal file
548
src/runtime/run.rs
Normal file
@@ -0,0 +1,548 @@
|
||||
// Top-level CLI execution pipeline.
|
||||
|
||||
pub(crate) fn run_config(args: RunConfig) -> Result<(), String> {
|
||||
let mut policy = read_policy(args.policy_path.as_deref())?;
|
||||
if let Some(strict_policy) = args.strict_policy {
|
||||
policy.strict = strict_policy;
|
||||
}
|
||||
if let Some(resource_validation_mode) = args.resource_validation_mode {
|
||||
policy.resource_validation_mode = resource_validation_mode;
|
||||
}
|
||||
if args.disable_rrdp {
|
||||
policy.sync_preference = crate::validation::policy::SyncPreference::RsyncOnly;
|
||||
}
|
||||
policy.ta_constraints = args.ta_constraints.clone();
|
||||
for warning in policy.ta_constraints.configuration_warnings() {
|
||||
crate::logging::emit(crate::logging::Level::Warn, "constraint_warning", || serde_json::json!({"reason": warning}));
|
||||
}
|
||||
let validation_time = args
|
||||
.validation_time
|
||||
.unwrap_or_else(time::OffsetDateTime::now_utc);
|
||||
let validation_time =
|
||||
time::OffsetDateTime::from_unix_timestamp(validation_time.unix_timestamp())
|
||||
.map_err(|error| format!("normalize validation time failed: {error}"))?;
|
||||
let http_root_certificates_pem = args
|
||||
.http_root_cert_paths
|
||||
.iter()
|
||||
.map(|path| {
|
||||
std::fs::read(path)
|
||||
.map_err(|e| format!("read HTTP root certificate failed: {}: {e}", path.display()))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let store = if args.raw_store_db.is_some() || args.repo_bytes_db.is_some() {
|
||||
Arc::new(
|
||||
RocksStore::open_with_external_stores(
|
||||
&args.db_path,
|
||||
args.raw_store_db.as_deref(),
|
||||
args.repo_bytes_db.as_deref(),
|
||||
)
|
||||
.map_err(|e| e.to_string())?,
|
||||
)
|
||||
} else {
|
||||
Arc::new(RocksStore::open(&args.db_path).map_err(|e| e.to_string())?)
|
||||
};
|
||||
let config = TreeRunConfig {
|
||||
max_depth: Some(args.max_ca_depth),
|
||||
max_instances: args.max_instances,
|
||||
compact_audit: args.skip_report_build
|
||||
&& args.report_json_path.is_none(),
|
||||
build_ccr_accumulator: args.ccr_out_path.is_some(),
|
||||
};
|
||||
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
let mut timing: Option<(std::path::PathBuf, TimingHandle)> = None;
|
||||
if args.analyze {
|
||||
let recorded_at_utc_rfc3339 = time::OffsetDateTime::now_utc()
|
||||
.to_offset(time::UtcOffset::UTC)
|
||||
.format(&Rfc3339)
|
||||
.map_err(|e| format!("format recorded_at_utc failed: {e}"))?;
|
||||
let validation_time_utc_rfc3339 = validation_time
|
||||
.to_offset(time::UtcOffset::UTC)
|
||||
.format(&Rfc3339)
|
||||
.map_err(|e| format!("format validation_time failed: {e}"))?;
|
||||
|
||||
let ts_compact = {
|
||||
let fmt = time::format_description::parse_borrowed::<2>(
|
||||
"[year][month][day]T[hour][minute][second]Z",
|
||||
)
|
||||
.map_err(|e| format!("format description parse failed: {e}"))?;
|
||||
time::OffsetDateTime::now_utc()
|
||||
.format(&fmt)
|
||||
.map_err(|e| format!("format timestamp failed: {e}"))?
|
||||
};
|
||||
|
||||
let out_dir = args.analysis_out_path.clone().unwrap_or_else(|| {
|
||||
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("target")
|
||||
.join("live")
|
||||
.join("analyze")
|
||||
.join(ts_compact)
|
||||
});
|
||||
std::fs::create_dir_all(&out_dir)
|
||||
.map_err(|e| format!("create analyze out dir failed: {}: {e}", out_dir.display()))?;
|
||||
|
||||
let handle = TimingHandle::new(TimingMeta {
|
||||
recorded_at_utc_rfc3339,
|
||||
validation_time_utc_rfc3339,
|
||||
tal_url: None,
|
||||
db_path: None,
|
||||
});
|
||||
handle.set_meta(TimingMetaUpdate {
|
||||
tal_url: args.tal_url.as_deref(),
|
||||
db_path: Some(args.db_path.to_string_lossy().as_ref()),
|
||||
});
|
||||
timing = Some((out_dir, handle));
|
||||
}
|
||||
|
||||
let total_started = std::time::Instant::now();
|
||||
let mut memory_checkpoints: Vec<MemoryTelemetryCheckpoint> = Vec::new();
|
||||
let mut malloc_trim_probes: Vec<MallocTrimProbe> = Vec::new();
|
||||
let enable_memory_trim_probe = memory_trim_probe_enabled() || args.memory_trim_after_validation;
|
||||
record_memory_checkpoint(
|
||||
&mut memory_checkpoints,
|
||||
"after_store_open",
|
||||
&total_started,
|
||||
store.as_ref(),
|
||||
);
|
||||
let validation_started = std::time::Instant::now();
|
||||
let collect_current_repo_objects = false;
|
||||
let out = if let Some(dir) = args.rsync_local_dir.as_ref() {
|
||||
let http = BlockingHttpFetcher::new(HttpFetcherConfig {
|
||||
timeout: std::time::Duration::from_secs(args.http_timeout_secs.max(1)),
|
||||
extra_root_certificates_pem: http_root_certificates_pem.clone(),
|
||||
..HttpFetcherConfig::default()
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
let rsync = LocalDirRsyncFetcher::new(dir);
|
||||
run_online_validation_with_fetchers(
|
||||
Arc::clone(&store),
|
||||
&policy,
|
||||
&args,
|
||||
&http,
|
||||
&rsync,
|
||||
validation_time,
|
||||
&config,
|
||||
collect_current_repo_objects,
|
||||
timing.as_ref().map(|(_, t)| t),
|
||||
)?
|
||||
} else {
|
||||
let http = BlockingHttpFetcher::new(HttpFetcherConfig {
|
||||
timeout: std::time::Duration::from_secs(args.http_timeout_secs.max(1)),
|
||||
extra_root_certificates_pem: http_root_certificates_pem.clone(),
|
||||
..HttpFetcherConfig::default()
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
let rsync = SystemRsyncFetcher::new(SystemRsyncConfig {
|
||||
rsync_bin: args
|
||||
.rsync_command
|
||||
.clone()
|
||||
.unwrap_or_else(|| PathBuf::from("rsync")),
|
||||
timeout: std::time::Duration::from_secs(args.rsync_timeout_secs.max(1)),
|
||||
mirror_root: args.rsync_mirror_root.clone(),
|
||||
scope_policy: args.rsync_scope_policy,
|
||||
..SystemRsyncConfig::default()
|
||||
});
|
||||
run_online_validation_with_fetchers(
|
||||
Arc::clone(&store),
|
||||
&policy,
|
||||
&args,
|
||||
&http,
|
||||
&rsync,
|
||||
validation_time,
|
||||
&config,
|
||||
collect_current_repo_objects,
|
||||
timing.as_ref().map(|(_, t)| t),
|
||||
)?
|
||||
};
|
||||
|
||||
let validation_ms = validation_started.elapsed().as_millis() as u64;
|
||||
crate::logging::emit(crate::logging::Level::Info, "validation_phase_completed", || serde_json::json!({"elapsed_ms": validation_ms}));
|
||||
let shared = PostValidationShared::from_run_output(out);
|
||||
record_memory_checkpoint(
|
||||
&mut memory_checkpoints,
|
||||
"after_validation",
|
||||
&total_started,
|
||||
store.as_ref(),
|
||||
);
|
||||
if enable_memory_trim_probe {
|
||||
malloc_trim_probes.push(crate::output::memory::malloc_trim_probe());
|
||||
record_memory_checkpoint(
|
||||
&mut memory_checkpoints,
|
||||
"after_validation_malloc_trim",
|
||||
&total_started,
|
||||
store.as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some((_out_dir, t)) = timing.as_ref() {
|
||||
t.record_count("instances_processed", shared.instances_processed as u64);
|
||||
t.record_count("instances_failed", shared.instances_failed as u64);
|
||||
}
|
||||
|
||||
let publication_points = shared.publication_points.len();
|
||||
let publication_point_repo_sync_ms_total: u64 = shared
|
||||
.publication_points
|
||||
.iter()
|
||||
.map(|pp| pp.repo_sync_duration_ms.unwrap_or(0))
|
||||
.sum();
|
||||
let download_event_count = shared.download_stats.events_total;
|
||||
let rrdp_download_ms_total: u64 = ["rrdp_notification", "rrdp_snapshot", "rrdp_delta"]
|
||||
.iter()
|
||||
.map(|key| {
|
||||
shared
|
||||
.download_stats
|
||||
.by_kind
|
||||
.get(*key)
|
||||
.map(|item| item.duration_ms_total)
|
||||
.unwrap_or(0)
|
||||
})
|
||||
.sum();
|
||||
let rsync_download_ms_total = shared
|
||||
.download_stats
|
||||
.by_kind
|
||||
.get("rsync")
|
||||
.map(|item| item.duration_ms_total)
|
||||
.unwrap_or(0);
|
||||
let repo_sync_ms_total = rrdp_download_ms_total + rsync_download_ms_total;
|
||||
let download_bytes_total: u64 = shared
|
||||
.download_stats
|
||||
.by_kind
|
||||
.values()
|
||||
.map(|item| item.bytes_total.unwrap_or(0))
|
||||
.sum();
|
||||
|
||||
let report_json_format = if args.report_json_compact {
|
||||
ReportJsonFormat::Compact
|
||||
} else {
|
||||
ReportJsonFormat::Pretty
|
||||
};
|
||||
let ccr_produced_at = time::OffsetDateTime::now_utc();
|
||||
let compare_view_trust_anchor = args
|
||||
.compare_view_trust_anchor
|
||||
.as_deref()
|
||||
.unwrap_or("unknown");
|
||||
let (report_result, ccr_result, compare_view_result) =
|
||||
std::thread::scope(|scope| {
|
||||
// Reborrow `shared` as a plain reference so the scoped output tasks
|
||||
// capture the reference instead of moving fields out of the owned value.
|
||||
let shared = &shared;
|
||||
let report_handle = if args.skip_report_build {
|
||||
None
|
||||
} else {
|
||||
Some(scope.spawn(|| {
|
||||
run_report_task(
|
||||
&policy,
|
||||
validation_time,
|
||||
shared,
|
||||
args.report_json_path.as_deref(),
|
||||
report_json_format,
|
||||
)
|
||||
}))
|
||||
};
|
||||
let ccr_handle = scope.spawn(|| {
|
||||
run_ccr_task(
|
||||
shared,
|
||||
args.ccr_out_path.as_deref(),
|
||||
ccr_produced_at,
|
||||
)
|
||||
});
|
||||
let compare_view_handle = scope.spawn(|| {
|
||||
run_compare_view_task(
|
||||
shared,
|
||||
args.vrps_csv_out_path.as_deref(),
|
||||
args.vaps_csv_out_path.as_deref(),
|
||||
compare_view_trust_anchor,
|
||||
)
|
||||
});
|
||||
let report_result = match report_handle {
|
||||
Some(handle) => handle
|
||||
.join()
|
||||
.map_err(|_| "report task panicked".to_string())
|
||||
.and_then(|result| result),
|
||||
None => Ok(ReportTaskOutput::skipped()),
|
||||
};
|
||||
let ccr_result = ccr_handle
|
||||
.join()
|
||||
.map_err(|_| "ccr task panicked".to_string())
|
||||
.and_then(|result| result);
|
||||
let compare_view_result = compare_view_handle
|
||||
.join()
|
||||
.map_err(|_| "compare view task panicked".to_string())
|
||||
.and_then(|result| result);
|
||||
(report_result, ccr_result, compare_view_result)
|
||||
});
|
||||
let report_output = report_result?;
|
||||
let ccr_output = ccr_result?;
|
||||
let compare_view_output = compare_view_result?;
|
||||
record_memory_checkpoint(
|
||||
&mut memory_checkpoints,
|
||||
"after_report_and_ccr",
|
||||
&total_started,
|
||||
store.as_ref(),
|
||||
);
|
||||
if enable_memory_trim_probe {
|
||||
malloc_trim_probes.push(crate::output::memory::malloc_trim_probe());
|
||||
record_memory_checkpoint(
|
||||
&mut memory_checkpoints,
|
||||
"after_report_and_ccr_malloc_trim",
|
||||
&total_started,
|
||||
store.as_ref(),
|
||||
);
|
||||
}
|
||||
let report_build_ms = report_output.report_build_ms;
|
||||
let report_write_ms = report_output.report_write_ms;
|
||||
let ccr_build_ms = ccr_output.ccr_build_ms;
|
||||
let ccr_write_ms = ccr_output.ccr_write_ms;
|
||||
let compare_view_build_ms = compare_view_output.build_ms;
|
||||
let compare_view_write_ms = compare_view_output.write_ms;
|
||||
record_memory_checkpoint(
|
||||
&mut memory_checkpoints,
|
||||
"after_compare_view",
|
||||
&total_started,
|
||||
store.as_ref(),
|
||||
);
|
||||
|
||||
record_memory_checkpoint(
|
||||
&mut memory_checkpoints,
|
||||
"before_stage_timing",
|
||||
&total_started,
|
||||
store.as_ref(),
|
||||
);
|
||||
let timing_report_snapshot = timing
|
||||
.as_ref()
|
||||
.map(|(_, handle)| handle.report_snapshot(50));
|
||||
let stage_timing = RunStageTiming {
|
||||
validation_ms,
|
||||
report_build_ms,
|
||||
report_write_ms,
|
||||
ccr_build_ms,
|
||||
ccr_write_ms,
|
||||
compare_view_build_ms,
|
||||
compare_view_write_ms,
|
||||
total_ms: total_started.elapsed().as_millis() as u64,
|
||||
publication_points,
|
||||
repo_sync_ms_total,
|
||||
publication_point_repo_sync_ms_total,
|
||||
download_event_count,
|
||||
rrdp_download_ms_total,
|
||||
rsync_download_ms_total,
|
||||
download_bytes_total,
|
||||
analysis_counts: timing
|
||||
.as_ref()
|
||||
.map(|(_, handle)| handle.counts_snapshot())
|
||||
.unwrap_or_default(),
|
||||
analysis_phases: timing_report_snapshot
|
||||
.as_ref()
|
||||
.map(|report| report.phases.clone())
|
||||
.unwrap_or_default(),
|
||||
analysis_top_publication_points: timing_report_snapshot
|
||||
.as_ref()
|
||||
.map(|report| report.top_publication_points.clone())
|
||||
.unwrap_or_default(),
|
||||
analysis_top_publication_point_steps: timing_report_snapshot
|
||||
.as_ref()
|
||||
.map(|report| report.top_publication_point_steps.clone())
|
||||
.unwrap_or_default(),
|
||||
memory_telemetry: Some(MemoryTelemetrySummary {
|
||||
checkpoints: memory_checkpoints,
|
||||
object_graph: Some(estimate_shared_object_graph(&shared)),
|
||||
malloc_trim_probes,
|
||||
}),
|
||||
};
|
||||
let stage_timing_anchor_path = args
|
||||
.report_json_path
|
||||
.as_deref()
|
||||
.or(args.ccr_out_path.as_deref())
|
||||
.or(args.vrps_csv_out_path.as_deref());
|
||||
write_stage_timing(stage_timing_anchor_path, &stage_timing)?;
|
||||
|
||||
if let Some((out_dir, t)) = timing.as_ref() {
|
||||
t.record_count("vrps", shared.vrps.len() as u64);
|
||||
t.record_count("aspas", shared.aspas.len() as u64);
|
||||
t.record_count(
|
||||
"audit_publication_points",
|
||||
shared.publication_points.len() as u64,
|
||||
);
|
||||
let timing_json_path = out_dir.join("timing.json");
|
||||
t.write_json(&timing_json_path, 20)?;
|
||||
crate::logging::info!("analysis: wrote {}", timing_json_path.display());
|
||||
}
|
||||
|
||||
// Write the compatibility summary after analysis timing has been flushed.
|
||||
// The RRDP counters (notably delta operations) are recorded by
|
||||
// the transport/validation stages rather than by the compact publication
|
||||
// point audit, so the final summary can use those authoritative counts.
|
||||
if let Some(path) = args.summary_out_path.as_deref() {
|
||||
let analysis_counts = timing
|
||||
.as_ref()
|
||||
.map(|(_, handle)| handle.counts_snapshot());
|
||||
write_summary(
|
||||
path,
|
||||
&shared,
|
||||
&args,
|
||||
validation_ms,
|
||||
analysis_counts.as_ref(),
|
||||
)?;
|
||||
}
|
||||
|
||||
print_summary_from_shared(validation_time, &shared);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ValidationSummary {
|
||||
backend: &'static str,
|
||||
worker_count: usize,
|
||||
repo_sync_worker_count: usize,
|
||||
trust_anchors: usize,
|
||||
synchronized_repositories: usize,
|
||||
synchronized_objects: usize,
|
||||
validated_ca_certificates: usize,
|
||||
validated_manifests: usize,
|
||||
validated_crls: usize,
|
||||
validated_roas: usize,
|
||||
rejected_ca_certificates: usize,
|
||||
rejected_manifests: usize,
|
||||
rejected_crls: usize,
|
||||
rejected_roas: usize,
|
||||
rrdp_snapshot_repositories: usize,
|
||||
rrdp_snapshot_fallbacks: usize,
|
||||
rrdp_delta_repositories: usize,
|
||||
rrdp_noop_repositories: usize,
|
||||
rrdp_delta_updates: usize,
|
||||
vrps: usize,
|
||||
publication_points: usize,
|
||||
validation_ms: u64,
|
||||
}
|
||||
|
||||
fn write_summary(
|
||||
path: &Path,
|
||||
shared: &PostValidationShared,
|
||||
args: &RunConfig,
|
||||
validation_ms: u64,
|
||||
analysis_counts: Option<&std::collections::HashMap<String, u64>>,
|
||||
) -> Result<(), String> {
|
||||
use crate::output::audit::{AuditDownloadKind, AuditObjectKind, AuditObjectResult};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
let mut summary = ValidationSummary {
|
||||
backend: "panda-rpki",
|
||||
worker_count: args.parallel_phase2_config.object_workers,
|
||||
repo_sync_worker_count: args.parallel_phase1_config.max_repo_sync_workers_global,
|
||||
trust_anchors: shared.discoveries.len().max(1),
|
||||
synchronized_repositories: 0,
|
||||
synchronized_objects: 0,
|
||||
validated_ca_certificates: shared.instances_processed,
|
||||
validated_manifests: 0,
|
||||
validated_crls: 0,
|
||||
validated_roas: 0,
|
||||
rejected_ca_certificates: shared.instances_failed,
|
||||
rejected_manifests: 0,
|
||||
rejected_crls: 0,
|
||||
rejected_roas: 0,
|
||||
rrdp_snapshot_repositories: 0,
|
||||
rrdp_snapshot_fallbacks: 0,
|
||||
rrdp_delta_repositories: 0,
|
||||
rrdp_noop_repositories: 0,
|
||||
rrdp_delta_updates: 0,
|
||||
vrps: shared.vrps.len(),
|
||||
publication_points: shared.publication_points.len(),
|
||||
validation_ms,
|
||||
};
|
||||
let mut notification_uris = BTreeSet::new();
|
||||
for point in shared.publication_points.iter() {
|
||||
if let Some(uri) = point.rrdp_notification_uri.as_deref() {
|
||||
notification_uris.insert(uri);
|
||||
}
|
||||
summary.synchronized_objects += point.objects.len();
|
||||
match point.repo_sync_phase.as_deref() {
|
||||
Some(phase) if phase.contains("fallback") => summary.rrdp_snapshot_fallbacks += 1,
|
||||
Some(phase) if phase.contains("snapshot") => {
|
||||
summary.rrdp_snapshot_repositories += 1
|
||||
}
|
||||
Some(phase) if phase.contains("delta") => summary.rrdp_delta_repositories += 1,
|
||||
Some(phase) if phase.contains("noop") => summary.rrdp_noop_repositories += 1,
|
||||
_ => {}
|
||||
}
|
||||
for object in &point.objects {
|
||||
let accepted = object.result == AuditObjectResult::Ok;
|
||||
match &object.kind {
|
||||
AuditObjectKind::Manifest => {
|
||||
if accepted { summary.validated_manifests += 1; } else { summary.rejected_manifests += 1; }
|
||||
}
|
||||
AuditObjectKind::Crl => {
|
||||
if accepted { summary.validated_crls += 1; } else { summary.rejected_crls += 1; }
|
||||
}
|
||||
AuditObjectKind::Roa => {
|
||||
if accepted { summary.validated_roas += 1; } else { summary.rejected_roas += 1; }
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Compact audit mode is not used by the Panda RPKI frontend, but deriving the
|
||||
// transport counters from download statistics keeps the summary correct
|
||||
// even when a future caller chooses a reduced publication-point audit.
|
||||
let download_stat = |kind: &str| shared.download_stats.by_kind.get(kind);
|
||||
if let Some(stats) = download_stat("rrdp_snapshot") {
|
||||
summary.rrdp_snapshot_repositories = stats.ok_total as usize;
|
||||
}
|
||||
if let Some(stats) = download_stat("rrdp_delta") {
|
||||
summary.rrdp_delta_repositories = stats.ok_total as usize;
|
||||
let counted_delta_updates = stats.objects_count_total.unwrap_or(0);
|
||||
summary.rrdp_delta_updates = if counted_delta_updates > 0 {
|
||||
counted_delta_updates as usize
|
||||
} else {
|
||||
analysis_counts
|
||||
.and_then(|counts| counts.get("rrdp_delta_ops_applied_total"))
|
||||
.copied()
|
||||
.unwrap_or_else(|| {
|
||||
shared
|
||||
.downloads
|
||||
.iter()
|
||||
.filter(|event| event.kind == AuditDownloadKind::RrdpDelta && event.success)
|
||||
.filter_map(|event| event.objects.as_ref())
|
||||
.map(|objects| objects.objects_count)
|
||||
.sum()
|
||||
}) as usize
|
||||
};
|
||||
}
|
||||
if let Some(stats) = download_stat("rsync") {
|
||||
summary.rrdp_snapshot_fallbacks = stats.ok_total as usize;
|
||||
}
|
||||
if let Some(stats) = download_stat("rrdp_notification") {
|
||||
let transport_repositories = stats.ok_total as usize;
|
||||
summary.rrdp_noop_repositories = transport_repositories
|
||||
.saturating_sub(summary.rrdp_snapshot_repositories)
|
||||
.saturating_sub(summary.rrdp_delta_repositories)
|
||||
.saturating_sub(summary.rrdp_snapshot_fallbacks);
|
||||
}
|
||||
let downloaded_objects = analysis_counts
|
||||
.and_then(|counts| {
|
||||
let snapshot = counts.get("rrdp_snapshot_objects_applied_total").copied();
|
||||
let delta = counts.get("rrdp_delta_ops_applied_total").copied();
|
||||
snapshot.zip(delta).map(|(snapshot, delta)| snapshot + delta)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
shared
|
||||
.download_stats
|
||||
.by_kind
|
||||
.values()
|
||||
.filter_map(|stats| stats.objects_count_total)
|
||||
.sum()
|
||||
});
|
||||
if downloaded_objects > 0 {
|
||||
summary.synchronized_objects = downloaded_objects as usize;
|
||||
}
|
||||
summary.synchronized_repositories = notification_uris.len();
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|error| format!("create Panda RPKI summary parent {}: {error}", parent.display()))?;
|
||||
}
|
||||
let bytes = serde_json::to_vec_pretty(&summary)
|
||||
.map_err(|error| format!("serialize Panda RPKI summary: {error}"))?;
|
||||
std::fs::write(path, bytes)
|
||||
.map_err(|error| format!("write Panda RPKI summary {}: {error}", path.display()))
|
||||
}
|
||||
Reference in New Issue
Block a user