use clap::Args;
use monocle::database::MonocleDatabase;
use monocle::lens::as2rel::{As2relLens, As2relSearchArgs};
use monocle::utils::{truncate_name, OutputFormat, DEFAULT_NAME_MAX_LEN};
use monocle::MonocleConfig;
use serde::Serialize;
use serde_json::json;
use std::time::Duration;
use tabled::settings::Style;
use tabled::Table;
#[derive(Args)]
pub struct As2relArgs {
#[clap(required = true)]
pub asns: Vec<u32>,
#[clap(short, long)]
pub update: bool,
#[clap(long)]
pub update_with: Option<String>,
#[clap(long)]
pub no_explain: bool,
#[clap(long)]
pub sort_by_asn: bool,
#[clap(long)]
pub show_name: bool,
#[clap(long)]
pub show_full_name: bool,
#[clap(long, value_name = "PERCENT")]
pub min_visibility: Option<f32>,
#[clap(long)]
pub single_homed: bool,
#[clap(long, conflicts_with_all = ["is_downstream", "is_peer"])]
pub is_upstream: bool,
#[clap(long, conflicts_with_all = ["is_upstream", "is_peer"])]
pub is_downstream: bool,
#[clap(long, conflicts_with_all = ["is_upstream", "is_downstream"])]
pub is_peer: bool,
}
pub fn run(config: &MonocleConfig, args: As2relArgs, output_format: OutputFormat, no_update: bool) {
let As2relArgs {
asns,
update,
update_with,
no_explain,
sort_by_asn,
show_name,
show_full_name,
min_visibility,
single_homed,
is_upstream,
is_downstream,
is_peer,
} = args;
let show_name = show_name || show_full_name;
if asns.is_empty() {
eprintln!("ERROR: Please provide at least one ASN");
std::process::exit(1);
}
if asns.len() != 1 {
if single_homed {
eprintln!("ERROR: --single-homed can only be used with a single ASN");
std::process::exit(1);
}
if is_upstream || is_downstream || is_peer {
eprintln!(
"ERROR: --is-upstream, --is-downstream, and --is-peer can only be used with a single ASN"
);
std::process::exit(1);
}
}
if let Some(min_vis) = min_visibility {
if !(0.0..=100.0).contains(&min_vis) {
eprintln!("ERROR: --min-visibility must be between 0 and 100");
std::process::exit(1);
}
}
let sqlite_path = config.sqlite_path();
if update || update_with.is_some() {
if no_update {
eprintln!("[monocle] Warning: --update ignored because --no-update is set");
} else {
eprintln!("[monocle] Updating AS2rel data...");
let db = match MonocleDatabase::open(&sqlite_path) {
Ok(db) => db,
Err(e) => {
eprintln!("Failed to open database: {}", e);
std::process::exit(1);
}
};
let lens = As2relLens::with_ttl(&db, config.as2rel_cache_ttl());
let result = match &update_with {
Some(path) => lens.update_from(path),
None => lens.update(),
};
match result {
Ok(count) => {
eprintln!(
"[monocle] AS2rel data updated: {} relationships loaded",
count
);
}
Err(e) => {
eprintln!("[monocle] Failed to update AS2rel data: {}", e);
std::process::exit(1);
}
}
run_query(
&db,
&asns,
sort_by_asn,
show_name,
show_full_name,
no_explain,
output_format,
min_visibility,
single_homed,
is_upstream,
is_downstream,
is_peer,
config.as2rel_cache_ttl(),
);
return;
}
}
let db = match MonocleDatabase::open(&sqlite_path) {
Ok(db) => db,
Err(e) => {
eprintln!("Failed to open database: {}", e);
std::process::exit(1);
}
};
let lens = As2relLens::with_ttl(&db, config.as2rel_cache_ttl());
if let Some(reason) = lens.update_reason() {
if no_update {
eprintln!(
"[monocle] Warning: AS2rel {} Results may be incomplete.",
reason
);
eprintln!("[monocle] Run without --no-update or use 'monocle config update --as2rel' to load data.");
} else {
eprintln!("[monocle] AS2rel {}, updating now...", reason);
match lens.update() {
Ok(count) => {
eprintln!(
"[monocle] AS2rel data updated: {} relationships loaded",
count
);
}
Err(e) => {
eprintln!("[monocle] Failed to update AS2rel data: {}", e);
std::process::exit(1);
}
}
}
}
run_query(
&db,
&asns,
sort_by_asn,
show_name,
show_full_name,
no_explain,
output_format,
min_visibility,
single_homed,
is_upstream,
is_downstream,
is_peer,
config.as2rel_cache_ttl(),
);
}
#[derive(Debug, Clone, Serialize, tabled::Tabled)]
struct As2relResult {
asn1: u32,
asn2: u32,
connected: String,
peer: String,
as1_upstream: String,
as2_upstream: String,
}
#[derive(Debug, Clone, Serialize, tabled::Tabled)]
struct As2relResultWithName {
asn1: u32,
asn2: u32,
asn2_name: String,
connected: String,
peer: String,
as1_upstream: String,
as2_upstream: String,
}
#[allow(clippy::too_many_arguments)]
fn run_query(
db: &MonocleDatabase,
asns: &[u32],
sort_by_asn: bool,
show_name: bool,
show_full_name: bool,
no_explain: bool,
output_format: OutputFormat,
min_visibility: Option<f32>,
single_homed: bool,
is_upstream: bool,
is_downstream: bool,
is_peer: bool,
ttl: Duration,
) {
let lens = As2relLens::with_ttl(db, ttl);
let search_args = As2relSearchArgs {
asns: asns.to_vec(),
sort_by_asn,
show_name,
no_explain,
min_visibility,
single_homed,
is_upstream,
is_downstream,
is_peer,
};
if let Err(e) = search_args.validate() {
eprintln!("ERROR: {}", e);
std::process::exit(1);
}
let results = match lens.search(&search_args) {
Ok(r) => r,
Err(e) => {
eprintln!("Error searching for AS relationships: {}", e);
std::process::exit(1);
}
};
if results.is_empty() {
if output_format.is_json() {
println!("[]");
} else if single_homed {
println!(
"No single-homed ASNs found for AS{} (with the current filters)",
asns[0]
);
} else if asns.len() == 1 {
let filter_msg = if is_upstream {
" with --is-upstream filter"
} else if is_downstream {
" with --is-downstream filter"
} else if is_peer {
" with --is-peer filter"
} else {
""
};
println!("No relationships found for ASN {}{}", asns[0], filter_msg);
} else if asns.len() == 2 {
println!(
"No relationship found between ASN {} and ASN {}",
asns[0], asns[1]
);
} else {
println!("No relationships found among the provided ASNs");
}
return;
}
if !no_explain && !output_format.is_json() {
if single_homed {
eprintln!("{}", lens.get_single_homed_explanation(asns[0]));
} else {
eprintln!("{}", lens.get_explanation());
}
}
let truncate_names = !show_full_name && output_format.is_table();
let max_peers = lens.get_max_peers_count();
match output_format {
OutputFormat::Table => {
if show_name {
let display: Vec<As2relResultWithName> = results
.iter()
.map(|r| As2relResultWithName {
asn1: r.asn1,
asn2: r.asn2,
asn2_name: format_name(&r.asn2_name, truncate_names),
connected: r.connected.clone(),
peer: r.peer.clone(),
as1_upstream: r.as1_upstream.clone(),
as2_upstream: r.as2_upstream.clone(),
})
.collect();
println!("{}", Table::new(display).with(Style::rounded()));
} else {
let display: Vec<As2relResult> = results
.iter()
.map(|r| As2relResult {
asn1: r.asn1,
asn2: r.asn2,
connected: r.connected.clone(),
peer: r.peer.clone(),
as1_upstream: r.as1_upstream.clone(),
as2_upstream: r.as2_upstream.clone(),
})
.collect();
println!("{}", Table::new(display).with(Style::rounded()));
}
}
OutputFormat::Markdown => {
if show_name {
let display: Vec<As2relResultWithName> = results
.iter()
.map(|r| As2relResultWithName {
asn1: r.asn1,
asn2: r.asn2,
asn2_name: format_name(&r.asn2_name, truncate_names),
connected: r.connected.clone(),
peer: r.peer.clone(),
as1_upstream: r.as1_upstream.clone(),
as2_upstream: r.as2_upstream.clone(),
})
.collect();
println!("{}", Table::new(display).with(Style::markdown()));
} else {
let display: Vec<As2relResult> = results
.iter()
.map(|r| As2relResult {
asn1: r.asn1,
asn2: r.asn2,
connected: r.connected.clone(),
peer: r.peer.clone(),
as1_upstream: r.as1_upstream.clone(),
as2_upstream: r.as2_upstream.clone(),
})
.collect();
println!("{}", Table::new(display).with(Style::markdown()));
}
}
OutputFormat::Json => {
let output = build_json_output(&results, show_name, max_peers);
match serde_json::to_string(&output) {
Ok(json) => println!("{}", json),
Err(e) => eprintln!("ERROR: Failed to serialize to JSON: {}", e),
}
}
OutputFormat::JsonPretty => {
let output = build_json_output(&results, show_name, max_peers);
match serde_json::to_string_pretty(&output) {
Ok(json) => println!("{}", json),
Err(e) => eprintln!("ERROR: Failed to serialize to JSON: {}", e),
}
}
OutputFormat::JsonLine => {
for r in &results {
let obj = if show_name {
json!({
"asn1": r.asn1,
"asn2": r.asn2,
"asn2_name": r.asn2_name.as_deref().unwrap_or(""),
"connected": &r.connected,
"peer": &r.peer,
"as1_upstream": &r.as1_upstream,
"as2_upstream": &r.as2_upstream,
})
} else {
json!({
"asn1": r.asn1,
"asn2": r.asn2,
"connected": &r.connected,
"peer": &r.peer,
"as1_upstream": &r.as1_upstream,
"as2_upstream": &r.as2_upstream,
})
};
match serde_json::to_string(&obj) {
Ok(json) => println!("{}", json),
Err(e) => eprintln!("ERROR: Failed to serialize to JSON: {}", e),
}
}
}
OutputFormat::Psv => {
if show_name {
println!("asn1|asn2|asn2_name|connected|peer|as1_upstream|as2_upstream");
for r in &results {
println!(
"{}|{}|{}|{}|{}|{}|{}",
r.asn1,
r.asn2,
r.asn2_name.as_deref().unwrap_or(""),
r.connected,
r.peer,
r.as1_upstream,
r.as2_upstream
);
}
} else {
println!("asn1|asn2|connected|peer|as1_upstream|as2_upstream");
for r in &results {
println!(
"{}|{}|{}|{}|{}|{}",
r.asn1, r.asn2, r.connected, r.peer, r.as1_upstream, r.as2_upstream
);
}
}
}
}
}
fn format_name(name: &Option<String>, truncate: bool) -> String {
let name = name.as_deref().unwrap_or("");
if truncate {
truncate_name(name, DEFAULT_NAME_MAX_LEN)
} else {
name.to_string()
}
}
fn build_json_output(
results: &[monocle::lens::as2rel::As2relSearchResult],
show_name: bool,
max_peers: u32,
) -> serde_json::Value {
let json_results: Vec<_> = results
.iter()
.map(|r| {
if show_name {
json!({
"asn1": r.asn1,
"asn2": r.asn2,
"asn2_name": r.asn2_name.as_deref().unwrap_or(""),
"connected": &r.connected,
"peer": &r.peer,
"as1_upstream": &r.as1_upstream,
"as2_upstream": &r.as2_upstream,
})
} else {
json!({
"asn1": r.asn1,
"asn2": r.asn2,
"connected": &r.connected,
"peer": &r.peer,
"as1_upstream": &r.as1_upstream,
"as2_upstream": &r.as2_upstream,
})
}
})
.collect();
json!({
"max_peers_count": max_peers,
"results": json_results,
})
}