rpki/src/bin/rrdp_state_dump.rs

304 lines
10 KiB
Rust

use std::path::PathBuf;
use rocksdb::{DB, IteratorMode, Options};
use rpki::storage::{
CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_URI_OWNER, RrdpSourceMemberRecord,
RrdpSourceRecord, RrdpUriOwnerRecord, column_family_descriptors,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DumpView {
Source,
Members,
Owners,
All,
}
impl DumpView {
fn parse(value: &str) -> Result<Self, String> {
match value {
"source" => Ok(Self::Source),
"members" => Ok(Self::Members),
"owners" => Ok(Self::Owners),
"all" => Ok(Self::All),
other => Err(format!(
"invalid --view: {other} (expected one of: source, members, owners, all)"
)),
}
}
}
#[derive(Debug)]
struct DumpArgs {
db_path: PathBuf,
view: DumpView,
}
fn usage() -> String {
let bin = "rrdp_state_dump";
format!(
"\
Usage:
{bin} --db <path> [--view <source|members|owners|all>]
Options:
--db <path> RocksDB directory
--view <name> Dump one RRDP view; default is all
--help Show this help
"
)
}
fn parse_args(argv: &[String]) -> Result<DumpArgs, String> {
if argv.iter().any(|a| a == "--help" || a == "-h") {
return Err(usage());
}
let mut db_path: Option<PathBuf> = None;
let mut view = DumpView::All;
let mut i = 1usize;
while i < argv.len() {
match argv[i].as_str() {
"--db" => {
i += 1;
let v = argv.get(i).ok_or("--db requires a value")?;
db_path = Some(PathBuf::from(v));
}
"--view" => {
i += 1;
let v = argv.get(i).ok_or("--view requires a value")?;
view = DumpView::parse(v)?;
}
other => return Err(format!("unknown argument: {other}\n\n{}", usage())),
}
i += 1;
}
Ok(DumpArgs {
db_path: db_path.ok_or_else(|| format!("--db is required\n\n{}", usage()))?,
view,
})
}
fn open_db(path: &std::path::Path) -> Result<DB, Box<dyn std::error::Error>> {
let mut opts = Options::default();
opts.create_if_missing(false);
opts.create_missing_column_families(false);
Ok(DB::open_cf_descriptors(
&opts,
path,
column_family_descriptors(),
)?)
}
fn collect_source_records(db: &DB) -> Result<Vec<RrdpSourceRecord>, Box<dyn std::error::Error>> {
let cf = db
.cf_handle(CF_RRDP_SOURCE)
.ok_or("missing column family: rrdp_source")?;
let mut out = Vec::new();
for res in db.iterator_cf(cf, IteratorMode::Start) {
let (_k, v) = res?;
let record: RrdpSourceRecord = serde_cbor::from_slice(&v)?;
record.validate_internal()?;
out.push(record);
}
out.sort_by(|a, b| a.notify_uri.cmp(&b.notify_uri));
Ok(out)
}
fn collect_source_member_records(
db: &DB,
) -> Result<Vec<RrdpSourceMemberRecord>, Box<dyn std::error::Error>> {
let cf = db
.cf_handle(CF_RRDP_SOURCE_MEMBER)
.ok_or("missing column family: rrdp_source_member")?;
let mut out = Vec::new();
for res in db.iterator_cf(cf, IteratorMode::Start) {
let (_k, v) = res?;
let record: RrdpSourceMemberRecord = serde_cbor::from_slice(&v)?;
record.validate_internal()?;
out.push(record);
}
out.sort_by(|a, b| {
a.notify_uri
.cmp(&b.notify_uri)
.then(a.rsync_uri.cmp(&b.rsync_uri))
});
Ok(out)
}
fn collect_uri_owner_records(
db: &DB,
) -> Result<Vec<RrdpUriOwnerRecord>, Box<dyn std::error::Error>> {
let cf = db
.cf_handle(CF_RRDP_URI_OWNER)
.ok_or("missing column family: rrdp_uri_owner")?;
let mut out = Vec::new();
for res in db.iterator_cf(cf, IteratorMode::Start) {
let (_k, v) = res?;
let record: RrdpUriOwnerRecord = serde_cbor::from_slice(&v)?;
record.validate_internal()?;
out.push(record);
}
out.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri));
Ok(out)
}
fn print_source_records(entries: &[RrdpSourceRecord]) {
println!("[source]");
println!("notify_uri\tlast_serial\tlast_session_id\tsync_state\tlast_snapshot_uri\tlast_error");
for record in entries {
println!(
"{}\t{}\t{}\t{:?}\t{}\t{}",
record.notify_uri,
record
.last_serial
.map(|v| v.to_string())
.unwrap_or_else(|| "-".to_string()),
record.last_session_id.as_deref().unwrap_or("-"),
record.sync_state,
record.last_snapshot_uri.as_deref().unwrap_or("-"),
record.last_error.as_deref().unwrap_or("-"),
);
}
}
fn print_source_member_records(entries: &[RrdpSourceMemberRecord]) {
println!("[members]");
println!("notify_uri\trsync_uri\tpresent\thash\tsession_id\tserial");
for record in entries {
println!(
"{}\t{}\t{}\t{}\t{}\t{}",
record.notify_uri,
record.rsync_uri,
record.present,
record.current_hash.as_deref().unwrap_or("-"),
record.last_confirmed_session_id,
record.last_confirmed_serial,
);
}
}
fn print_uri_owner_records(entries: &[RrdpUriOwnerRecord]) {
println!("[owners]");
println!("rsync_uri\tnotify_uri\towner_state\thash\tsession_id\tserial");
for record in entries {
println!(
"{}\t{}\t{:?}\t{}\t{}\t{}",
record.rsync_uri,
record.notify_uri,
record.owner_state,
record.current_hash.as_deref().unwrap_or("-"),
record.last_confirmed_session_id,
record.last_confirmed_serial,
);
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let argv: Vec<String> = std::env::args().collect();
let args = parse_args(&argv).map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
let db = open_db(&args.db_path)?;
match args.view {
DumpView::Source => print_source_records(&collect_source_records(&db)?),
DumpView::Members => print_source_member_records(&collect_source_member_records(&db)?),
DumpView::Owners => print_uri_owner_records(&collect_uri_owner_records(&db)?),
DumpView::All => {
print_source_records(&collect_source_records(&db)?);
print_source_member_records(&collect_source_member_records(&db)?);
print_uri_owner_records(&collect_uri_owner_records(&db)?);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use rpki::storage::{PackTime, RocksStore, RrdpSourceSyncState, RrdpUriOwnerState};
#[test]
fn parse_args_accepts_view_and_db() {
let args = parse_args(&[
"rrdp_state_dump".to_string(),
"--db".to_string(),
"db".to_string(),
"--view".to_string(),
"owners".to_string(),
])
.expect("parse args");
assert_eq!(args.db_path, PathBuf::from("db"));
assert_eq!(args.view, DumpView::Owners);
}
#[test]
fn parse_args_rejects_invalid_view() {
let err = parse_args(&[
"rrdp_state_dump".to_string(),
"--db".to_string(),
"db".to_string(),
"--view".to_string(),
"nope".to_string(),
])
.unwrap_err();
assert!(err.contains("invalid --view"), "{err}");
}
#[test]
fn collect_rrdp_views_reads_current_records() {
let dir = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(dir.path()).expect("open store");
store
.put_rrdp_source_record(&RrdpSourceRecord {
notify_uri: "https://example.test/notify.xml".to_string(),
last_session_id: Some("session-1".to_string()),
last_serial: Some(42),
first_seen_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
last_seen_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
last_sync_at: None,
sync_state: RrdpSourceSyncState::DeltaReady,
last_snapshot_uri: Some("https://example.test/snapshot.xml".to_string()),
last_snapshot_hash: None,
last_error: None,
})
.expect("put source record");
store
.put_rrdp_source_member_record(&RrdpSourceMemberRecord {
notify_uri: "https://example.test/notify.xml".to_string(),
rsync_uri: "rsync://example.test/repo/a.roa".to_string(),
current_hash: Some("11".repeat(32)),
object_type: Some("roa".to_string()),
present: true,
last_confirmed_session_id: "session-1".to_string(),
last_confirmed_serial: 42,
last_changed_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
})
.expect("put member record");
store
.put_rrdp_uri_owner_record(&RrdpUriOwnerRecord {
rsync_uri: "rsync://example.test/repo/a.roa".to_string(),
notify_uri: "https://example.test/notify.xml".to_string(),
current_hash: Some("11".repeat(32)),
last_confirmed_session_id: "session-1".to_string(),
last_confirmed_serial: 42,
last_changed_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
owner_state: RrdpUriOwnerState::Active,
})
.expect("put owner record");
drop(store);
let db = open_db(dir.path()).expect("open db");
let sources = collect_source_records(&db).expect("source dump");
let members = collect_source_member_records(&db).expect("members dump");
let owners = collect_uri_owner_records(&db).expect("owners dump");
assert_eq!(sources.len(), 1);
assert_eq!(sources[0].sync_state, RrdpSourceSyncState::DeltaReady);
assert_eq!(members.len(), 1);
assert_eq!(members[0].rsync_uri, "rsync://example.test/repo/a.roa");
assert_eq!(owners.len(), 1);
assert_eq!(owners[0].owner_state, RrdpUriOwnerState::Active);
}
}