Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ All notable changes to this project will be documented in this file.

* **Table formatting**: ASPA table output now wraps long provider lists at 60 characters for better readability

### Code Improvements

* Refactored CLI command modules: moved CLI argument definitions from main file to individual command submodules for better code organization and maintainability

### Dependencies

* Added `bgpkit-commons` v0.10 with features: `asinfo`, `rpki`, `countries`
Expand Down
26 changes: 5 additions & 21 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ path = "src/bin/monocle.rs"
[dependencies]
anyhow = "1.0"
bgpkit-broker = "0.10.1"
bgpkit-parser = { version = "0.12.0", features = ["serde"] }
bgpkit-parser = { version = "0.13.0", features = ["serde"] }
config = { version = "0.15", features = ["toml"] }
chrono = "0.4"
chrono-humanize = "0.2"
Expand All @@ -36,7 +36,6 @@ json_to_table = "0.12.0"
oneio = { version = "0.20.0", default-features = false, features = ["https", "gz", "bz", "json"] }
radar-rs = "0.1.0"
rayon = "1.8"
regex = "1.10"
bgpkit-commons = { version = "0.10", features = ["asinfo", "rpki", "countries"] }
rusqlite = { version = "0.37", features = ["bundled"] }
serde = { version = "1.0", features = ["derive"] }
Expand Down
144 changes: 144 additions & 0 deletions src/bin/commands/broker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use clap::Args;
use monocle::string_to_time;
use serde_json;
use tabled::settings::Style;
use tabled::{Table, Tabled};

/// Arguments for the Broker command
#[derive(Args)]
pub struct BrokerArgs {
/// starting timestamp (RFC3339 or unix epoch)
#[clap(long, short = 't')]
pub start_ts: String,

/// ending timestamp (RFC3339 or unix epoch)
#[clap(long, short = 'T')]
pub end_ts: String,

/// BGP collector name: e.g. rrc00, route-views2
#[clap(long, short = 'c')]
pub collector: Option<String>,

/// BGP collection project name, e.g. routeviews, or riperis
#[clap(long, short = 'P')]
pub project: Option<String>,

/// Data type, e.g., updates or rib
#[clap(long)]
pub data_type: Option<String>,

/// Page number to fetch (1-based). If set, only this page will be fetched.
#[clap(long)]
pub page: Option<i64>,

/// Page size for broker queries (default 1000)
#[clap(long)]
pub page_size: Option<i64>,
}

pub fn run(args: BrokerArgs, json: bool) {
let BrokerArgs {
start_ts,
end_ts,
collector,
project,
data_type,
page,
page_size,
} = args;

// parse time strings similar to Search subcommand
let ts_start = match string_to_time(&start_ts) {
Ok(t) => t.timestamp(),
Err(_) => {
eprintln!("start-ts is not a valid time string: {}", start_ts);
std::process::exit(1);
}
};
let ts_end = match string_to_time(&end_ts) {
Ok(t) => t.timestamp(),
Err(_) => {
eprintln!("end-ts is not a valid time string: {}", end_ts);
std::process::exit(1);
}
};

let mut broker = bgpkit_broker::BgpkitBroker::new()
.ts_start(ts_start)
.ts_end(ts_end);

if let Some(c) = collector {
broker = broker.collector_id(c.as_str());
}
if let Some(p) = project {
broker = broker.project(p.as_str());
}
if let Some(dt) = data_type {
broker = broker.data_type(dt.as_str());
}

let page_size = page_size.unwrap_or(1000);
broker = broker.page_size(page_size);

let res = if let Some(p) = page {
broker.page(p).query_single_page()
} else {
// Use query() and limit to at most 10 pages worth of items
match broker.query() {
Ok(mut v) => {
let max_items = (page_size * 10) as usize;
if v.len() > max_items {
v.truncate(max_items);
}
Ok(v)
}
Err(e) => Err(e),
}
};

match res {
Ok(items) => {
if items.is_empty() {
println!("No MRT files found");
return;
}

if json {
match serde_json::to_string_pretty(&items) {
Ok(json_str) => println!("{}", json_str),
Err(e) => eprintln!("error serializing: {}", e),
}
} else {
#[derive(Tabled)]
struct BrokerItemDisplay {
#[tabled(rename = "Collector")]
collector_id: String,
#[tabled(rename = "Type")]
data_type: String,
#[tabled(rename = "Start Time (UTC)")]
ts_start: String,
#[tabled(rename = "URL")]
url: String,
#[tabled(rename = "Size (Bytes)")]
rough_size: i64,
}

let display_items: Vec<BrokerItemDisplay> = items
.into_iter()
.map(|item| BrokerItemDisplay {
collector_id: item.collector_id,
data_type: item.data_type,
ts_start: item.ts_start.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
url: item.url,
rough_size: item.rough_size,
})
.collect();

println!("{}", Table::new(display_items).with(Style::markdown()));
}
}
Err(e) => {
eprintln!("failed to query: {}", e);
}
}
}
22 changes: 22 additions & 0 deletions src/bin/commands/country.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use clap::Args;
use monocle::{CountryEntry, CountryLookup};
use tabled::settings::Style;
use tabled::Table;

/// Arguments for the Country command
#[derive(Args)]
pub struct CountryArgs {
/// Search query, e.g. "US" or "United States"
pub queries: Vec<String>,
}

pub fn run(args: CountryArgs) {
let CountryArgs { queries } = args;

let lookup = CountryLookup::new();
let res: Vec<CountryEntry> = queries
.into_iter()
.flat_map(|query| lookup.lookup(query.as_str()))
.collect();
println!("{}", Table::new(res).with(Style::rounded()));
}
44 changes: 44 additions & 0 deletions src/bin/commands/ip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use clap::Args;
use json_to_table::json_to_table;
use monocle::fetch_ip_info;
use serde_json::json;
use std::net::IpAddr;

/// Arguments for the Ip command
#[derive(Args)]
pub struct IpArgs {
/// IP address to look up (optional)
#[clap()]
pub ip: Option<IpAddr>,

/// Print IP address only (e.g., for getting the public IP address quickly)
#[clap(long)]
pub simple: bool,
}

pub fn run(args: IpArgs, json: bool) {
let IpArgs { ip, simple } = args;

match fetch_ip_info(ip, simple) {
Ok(ipinfo) => {
if simple {
println!("{}", ipinfo.ip);
return;
}

let json_value = json!(&ipinfo);
if json {
if let Err(e) = serde_json::to_writer_pretty(std::io::stdout(), &json_value) {
eprintln!("Error writing JSON to stdout: {}", e);
}
} else {
let mut table = json_to_table(&json_value);
table.collapse();
println!("{}", table);
}
}
Err(e) => {
eprintln!("ERROR: unable to get ip information: {e}");
}
}
}
31 changes: 31 additions & 0 deletions src/bin/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
pub mod broker;
pub mod country;
pub mod ip;
pub mod parse;
pub mod pfx2as;
pub mod radar;
pub mod rpki;
pub mod search;
pub mod time;
pub mod whois;

pub(crate) fn elem_to_string(
elem: &bgpkit_parser::BgpElem,
json: bool,
pretty: bool,
collector: &str,
) -> Result<String, anyhow::Error> {
if json {
let mut val = serde_json::json!(elem);
val.as_object_mut()
.ok_or_else(|| anyhow::anyhow!("Expected JSON object"))?
.insert("collector".to_string(), collector.into());
if pretty {
Ok(serde_json::to_string_pretty(&val)?)
} else {
Ok(val.to_string())
}
} else {
Ok(format!("{}|{}", elem, collector))
}
}
Loading