304 lines
10 KiB
Rust
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);
|
|
}
|
|
}
|