easytier-cli部分命令支持json输出 (#882)

* add cli options to json output
* add cli verbose output in json format for some sub command

- easytier-cli -v peer list
- easytier-cli -v peer list-foreign
- easytier-cli -o json peer list-foreign
- easytier-cli -v peer list-global-foreign
- easytier-cli -o json peer list-global-foreign
- easytier-cli -v route list
- easytier-cli -v connector
- easytier-cli -o json connector
- easytier-cli -o json stun
- easytier-cli -v proxy
- easytier-cli -v node info

---------

Co-authored-by: xzzpig <w2xzzig@hotmail.com>
This commit is contained in:
Mg Pig
2025-05-25 23:28:12 +08:00
committed by GitHub
parent b0fd37949a
commit d7c3179c6e

View File

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