diff --git a/easytier/src/easytier-cli.rs b/easytier/src/easytier-cli.rs index f908585..0c68771 100644 --- a/easytier/src/easytier-cli.rs +++ b/easytier/src/easytier-cli.rs @@ -3,12 +3,14 @@ use std::{ fmt::Write, net::{IpAddr, SocketAddr}, path::PathBuf, + str::FromStr, sync::Mutex, time::Duration, vec, }; use anyhow::Context; +use cidr::Ipv4Inet; use clap::{command, Args, Parser, Subcommand}; use humansize::format_size; use service_manager::*; @@ -51,6 +53,15 @@ struct Cli { #[arg(short, long, default_value = "false", help = "verbose output")] verbose: bool, + #[arg( + short = 'o', + long = "output", + value_enum, + default_value = "table", + help = "output format" + )] + output_format: OutputFormat, + #[command(subcommand)] sub_command: SubCommand, } @@ -77,23 +88,23 @@ enum SubCommand { Proxy, } +#[derive(clap::ValueEnum, Debug, Clone, PartialEq)] +enum OutputFormat { + Table, + Json, +} + #[derive(Args, Debug)] struct PeerArgs { #[command(subcommand)] sub_command: Option, } -#[derive(Args, Debug)] -struct PeerListArgs { - #[arg(short, long)] - verbose: bool, -} - #[derive(Subcommand, Debug)] enum PeerSubCommand { Add, Remove, - List(PeerListArgs), + List, ListForeign, ListGlobalForeign, } @@ -193,14 +204,15 @@ struct InstallArgs { type Error = anyhow::Error; -struct CommandHandler { +struct CommandHandler<'a> { client: Mutex, verbose: bool, + output_format: &'a OutputFormat, } type RpcClient = StandAloneClient; -impl CommandHandler { +impl CommandHandler<'_> { async fn get_peer_manager_client( &self, ) -> Result>, Error> { @@ -294,9 +306,12 @@ impl CommandHandler { println!("remove peer"); } - async fn handle_peer_list(&self, _args: &PeerArgs) -> Result<(), Error> { - #[derive(tabled::Tabled)] + async fn handle_peer_list(&self) -> Result<(), Error> { + #[derive(tabled::Tabled, serde::Serialize)] struct PeerTableItem { + #[tabled(rename = "ipv4")] + cidr: String, + #[tabled(skip)] ipv4: String, hostname: String, cost: String, @@ -314,7 +329,12 @@ impl CommandHandler { fn from(p: PeerRoutePair) -> Self { let route = p.route.clone().unwrap_or_default(); PeerTableItem { - ipv4: route.ipv4_addr.map(|ip| ip.to_string()).unwrap_or_default(), + cidr: route.ipv4_addr.map(|ip| ip.to_string()).unwrap_or_default(), + ipv4: route + .ipv4_addr + .map(|ip: easytier::proto::common::Ipv4Inet| ip.address.unwrap_or_default()) + .map(|ip| ip.to_string()) + .unwrap_or_default(), hostname: route.hostname.clone(), cost: cost_to_str(route.cost), lat_ms: if route.cost == 1 { @@ -344,7 +364,10 @@ impl CommandHandler { impl From for PeerTableItem { fn from(p: NodeInfo) -> Self { PeerTableItem { - ipv4: p.ipv4_addr.clone(), + cidr: p.ipv4_addr.clone(), + ipv4: Ipv4Inet::from_str(&p.ipv4_addr) + .map(|ip| ip.address().to_string()) + .unwrap_or_default(), hostname: p.hostname.clone(), cost: "Local".to_string(), lat_ms: "-".to_string(), @@ -366,7 +389,7 @@ impl CommandHandler { let mut items: Vec = vec![]; let peer_routes = self.list_peer_route_pair().await?; if self.verbose { - println!("{:#?}", peer_routes); + println!("{}", serde_json::to_string_pretty(&peer_routes)?); return Ok(()); } @@ -382,7 +405,7 @@ impl CommandHandler { items.push(p.into()); } - println!("{}", tabled::Table::new(items).with(Style::modern())); + print_output(&items, self.output_format)?; Ok(()) } @@ -404,8 +427,9 @@ impl CommandHandler { .list_foreign_network(BaseController::default(), request) .await?; let network_map = response; - if self.verbose { - println!("{:#?}", network_map); + if self.verbose || *self.output_format == OutputFormat::Json { + let json = serde_json::to_string_pretty(&network_map.foreign_networks)?; + println!("{}", json); return Ok(()); } @@ -445,8 +469,11 @@ impl CommandHandler { let response = client .list_global_foreign_network(BaseController::default(), request) .await?; - if self.verbose { - println!("{:#?}", response); + if self.verbose || *self.output_format == OutputFormat::Json { + println!( + "{}", + serde_json::to_string_pretty(&response.foreign_networks)? + ); return Ok(()); } @@ -464,7 +491,7 @@ impl CommandHandler { } async fn handle_route_list(&self) -> Result<(), Error> { - #[derive(tabled::Tabled)] + #[derive(tabled::Tabled, serde::Serialize)] struct RouteTableItem { ipv4: String, hostname: String, @@ -491,6 +518,23 @@ impl CommandHandler { .await? .node_info .ok_or(anyhow::anyhow!("node info not found"))?; + let peer_routes = self.list_peer_route_pair().await?; + + if self.verbose { + #[derive(serde::Serialize)] + struct VerboseItem { + node_info: NodeInfo, + peer_routes: Vec, + } + println!( + "{}", + serde_json::to_string_pretty(&VerboseItem { + node_info, + peer_routes + })? + ); + return Ok(()); + } items.push(RouteTableItem { ipv4: node_info.ipv4_addr.clone(), @@ -510,7 +554,6 @@ impl CommandHandler { version: node_info.version.clone(), }); - let peer_routes = self.list_peer_route_pair().await?; for p in peer_routes.iter() { let Some(next_hop_pair) = peer_routes.iter().find(|pair| { pair.route.clone().unwrap_or_default().peer_id @@ -634,7 +677,7 @@ impl CommandHandler { } } - println!("{}", tabled::Table::new(items).with(Style::modern())); + print_output(&items, self.output_format)?; Ok(()) } @@ -645,6 +688,10 @@ impl CommandHandler { let response = client .list_connector(BaseController::default(), request) .await?; + if self.verbose || *self.output_format == OutputFormat::Json { + println!("{}", serde_json::to_string_pretty(&response.connectors)?); + return Ok(()); + } println!("response: {:#?}", response); Ok(()) } @@ -912,6 +959,21 @@ impl Service { } } +fn print_output(items: &[T], format: &OutputFormat) -> Result<(), Error> +where + T: tabled::Tabled + serde::Serialize, +{ + match format { + OutputFormat::Table => { + println!("{}", tabled::Table::new(items).with(Style::modern())); + } + OutputFormat::Json => { + println!("{}", serde_json::to_string_pretty(items)?); + } + } + Ok(()) +} + #[tokio::main] #[tracing::instrument] async fn main() -> Result<(), Error> { @@ -924,6 +986,7 @@ async fn main() -> Result<(), Error> { let handler = CommandHandler { client: Mutex::new(client), verbose: cli.verbose, + output_format: &cli.output_format, }; match cli.sub_command { @@ -934,12 +997,8 @@ async fn main() -> Result<(), Error> { Some(PeerSubCommand::Remove) => { println!("remove peer"); } - Some(PeerSubCommand::List(arg)) => { - if arg.verbose { - println!("{:#?}", handler.list_peer_route_pair().await?); - } else { - handler.handle_peer_list(&peer_args).await?; - } + Some(PeerSubCommand::List) => { + handler.handle_peer_list().await?; } Some(PeerSubCommand::ListForeign) => { handler.handle_foreign_network_list().await?; @@ -948,7 +1007,7 @@ async fn main() -> Result<(), Error> { handler.handle_global_foreign_network_list().await?; } None => { - handler.handle_peer_list(&peer_args).await?; + handler.handle_peer_list().await?; } }, SubCommand::Connector(conn_args) => match conn_args.sub_command { @@ -975,7 +1034,14 @@ async fn main() -> Result<(), Error> { loop { let ret = collector.get_stun_info(); if ret.udp_nat_type != NatType::Unknown as i32 { - println!("stun info: {:#?}", ret); + if cli.output_format == OutputFormat::Json { + match serde_json::to_string_pretty(&ret) { + Ok(json) => println!("{}", json), + Err(e) => eprintln!("Error serializing to JSON: {}", e), + } + } else { + println!("stun info: {:#?}", ret); + } break; } tokio::time::sleep(Duration::from_millis(200)).await; @@ -993,27 +1059,45 @@ async fn main() -> Result<(), Error> { ) .await?; - #[derive(tabled::Tabled)] + #[derive(tabled::Tabled, serde::Serialize)] struct PeerCenterTableItem { node_id: String, - direct_peers: String, + #[tabled(rename = "direct_peers")] + #[serde(skip_serializing)] + direct_peers_str: String, + #[tabled(skip)] + direct_peers: Vec, + } + + #[derive(serde::Serialize)] + struct DirectPeerItem { + node_id: String, + latency_ms: i32, } let mut table_rows = vec![]; for (k, v) in resp.global_peer_map.iter() { let node_id = k; - let direct_peers = v + let direct_peers_strs = v .direct_peers .iter() .map(|(k, v)| format!("{}: {:?}ms", k, v.latency_ms,)) .collect::>(); + let direct_peers: Vec<_> = v.direct_peers + .iter() + .map(|(k, v)| DirectPeerItem { + node_id: k.to_string(), + latency_ms: v.latency_ms, + }) + .collect(); table_rows.push(PeerCenterTableItem { node_id: node_id.to_string(), - direct_peers: direct_peers.join("\n"), + direct_peers_str: direct_peers_strs.join("\n"), + direct_peers, }); } - println!("{}", tabled::Table::new(table_rows).with(Style::modern())); + print_output(&table_rows, &cli.output_format)?; } SubCommand::VpnPortal => { let vpn_portal_client = handler.get_vpn_portal_client().await?; @@ -1045,6 +1129,11 @@ async fn main() -> Result<(), Error> { .ok_or(anyhow::anyhow!("node info not found"))?; match sub_cmd.sub_command { Some(NodeSubCommand::Info) | None => { + if cli.verbose || cli.output_format == OutputFormat::Json { + println!("{}", serde_json::to_string_pretty(&node_info)?); + return Ok(()); + } + let stun_info = node_info.stun_info.clone().unwrap_or_default(); let ip_list = node_info.ip_list.clone().unwrap_or_default(); @@ -1186,7 +1275,12 @@ async fn main() -> Result<(), Error> { .await; entries.extend(ret.unwrap_or_default().entries); - #[derive(tabled::Tabled)] + if cli.verbose { + println!("{}", serde_json::to_string_pretty(&entries)?); + return Ok(()); + } + + #[derive(tabled::Tabled, serde::Serialize)] struct TableItem { src: String, dst: String, @@ -1215,7 +1309,7 @@ async fn main() -> Result<(), Error> { }) .collect::>(); - println!("{}", tabled::Table::new(table_rows).with(Style::modern())); + print_output(&table_rows, &cli.output_format)?; } }