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 { 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 [--view ] Options: --db RocksDB directory --view Dump one RRDP view; default is all --help Show this help " ) } fn parse_args(argv: &[String]) -> Result { if argv.iter().any(|a| a == "--help" || a == "-h") { return Err(usage()); } let mut db_path: Option = 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> { 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, Box> { 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, Box> { 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, Box> { 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> { let argv: Vec = std::env::args().collect(); let args = parse_args(&argv).map_err(|e| -> Box { 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); } }