diff --git a/Cargo.lock b/Cargo.lock index 4b3c23b7..c11bd701 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -631,6 +631,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "compact_str" version = "0.7.1" @@ -2133,6 +2143,7 @@ dependencies = [ "chrono", "clap", "clap_complete", + "colored", "crossterm", "dialoguer", "dirs", @@ -2141,6 +2152,7 @@ dependencies = [ "expectorate", "futures", "httpmock", + "humantime", "indicatif", "log", "oauth2", diff --git a/Cargo.toml b/Cargo.toml index 032425c4..ee2431ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,8 @@ toml = "0.8.12" toml_edit = "0.22.12" url = "2.5.0" uuid = { version = "1.8.0", features = ["serde", "v4"] } +colored = "2.1.0" +humantime = "2" # Config for 'cargo dist' [workspace.metadata.dist] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 009a0e77..a0523b83 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -45,6 +45,8 @@ thouart = { workspace = true } tokio = { workspace = true } url = { workspace = true } uuid = { workspace = true } +colored = { workspace = true } +humantime = { workspace = true } [dev-dependencies] assert_cmd = { workspace = true } diff --git a/cli/docs/cli.json b/cli/docs/cli.json index 4697c838..f92e94ce 100644 --- a/cli/docs/cli.json +++ b/cli/docs/cli.json @@ -1966,6 +1966,33 @@ } ] }, + { + "name": "net", + "subcommands": [ + { + "name": "bgp", + "subcommands": [ + { + "name": "status", + "about": "Get the status of switch ports." + } + ] + }, + { + "name": "port", + "subcommands": [ + { + "name": "config", + "about": "Get the configuration of switch ports." + }, + { + "name": "status", + "about": "Get the status of switch ports." + } + ] + } + ] + }, { "name": "ping", "about": "Ping API", diff --git a/cli/src/cmd_net/bgp_status.rs b/cli/src/cmd_net/bgp_status.rs new file mode 100644 index 00000000..c4952a69 --- /dev/null +++ b/cli/src/cmd_net/bgp_status.rs @@ -0,0 +1,70 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::RunnableCmd; +use anyhow::Result; +use async_trait::async_trait; +use clap::Parser; +use colored::*; +use oxide::context::Context; +use oxide::types::{BgpPeerStatus, SwitchLocation}; +use oxide::ClientSystemNetworkingExt; +use std::io::Write; +use tabwriter::TabWriter; + +/// Get the status of switch ports. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "net bgp status")] +pub struct CmdBgpStatus {} + +#[async_trait] +impl RunnableCmd for CmdBgpStatus { + async fn run(&self, ctx: &Context) -> Result<()> { + let c = ctx.client()?; + + let status = c.networking_bgp_status().send().await?.into_inner(); + + let (sw0, sw1) = status + .iter() + .partition(|x| x.switch == SwitchLocation::Switch0); + + println!("{}", "switch0".dimmed()); + println!("{}", "=======".dimmed()); + show_status(&sw0)?; + println!(); + + println!("{}", "switch1".dimmed()); + println!("{}", "=======".dimmed()); + show_status(&sw1)?; + + Ok(()) + } +} + +fn show_status(st: &Vec<&BgpPeerStatus>) -> Result<()> { + let mut tw = TabWriter::new(std::io::stdout()).ansi(true); + writeln!( + &mut tw, + "{}\t{}\t{}\t{}\t{}", + "Peer Address".dimmed(), + "Local ASN".dimmed(), + "Remote ASN".dimmed(), + "Session State".dimmed(), + "State Duration".dimmed(), + )?; + for s in st { + writeln!( + tw, + "{}\t{}\t{}\t{:?}\t{}", + s.addr, + s.local_asn, + s.remote_asn, + s.state, + humantime::Duration::from(std::time::Duration::from_millis(s.state_duration_millis)), + )?; + } + tw.flush()?; + Ok(()) +} diff --git a/cli/src/cmd_net/mod.rs b/cli/src/cmd_net/mod.rs new file mode 100644 index 00000000..656b62e0 --- /dev/null +++ b/cli/src/cmd_net/mod.rs @@ -0,0 +1,15 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::cli_builder::NewCli; + +mod bgp_status; +mod port_config; +mod port_status; + +pub fn make_cli(cli: NewCli<'static>) -> NewCli<'static> { + cli.add_custom::("net port status") + .add_custom::("net port config") + .add_custom::("net bgp status") +} diff --git a/cli/src/cmd_net/port_config.rs b/cli/src/cmd_net/port_config.rs new file mode 100644 index 00000000..025616d2 --- /dev/null +++ b/cli/src/cmd_net/port_config.rs @@ -0,0 +1,170 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::io::Write; + +use crate::RunnableCmd; +use anyhow::Result; +use async_trait::async_trait; +use clap::Parser; +use colored::*; +use oxide::context::Context; +use oxide::{ClientSystemHardwareExt, ClientSystemNetworkingExt}; +use tabwriter::TabWriter; + +/// Get the configuration of switch ports. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "net port config")] +pub struct CmdPortConfig {} + +#[async_trait] +impl RunnableCmd for CmdPortConfig { + async fn run(&self, ctx: &Context) -> Result<()> { + let c = ctx.client()?; + + let ports = c + .networking_switch_port_list() + .limit(u32::MAX) + .send() + .await? + .into_inner() + .items; + + let mut tw = TabWriter::new(std::io::stdout()).ansi(true); + + // TODO bad API, having to pull all address lots to work backwards from + // address reference to address lot block is terribad + let addr_lots = c + .networking_address_lot_list() + .limit(u32::MAX) + .send() + .await? + .into_inner() + .items; + + let mut addr_lot_blocks = Vec::new(); + for a in addr_lots.iter() { + let blocks = c + .networking_address_lot_block_list() + .address_lot(a.id) + .limit(u32::MAX) + .send() + .await? + .into_inner() + .items; + + for b in blocks.iter() { + addr_lot_blocks.push((a.clone(), b.clone())); + } + } + + for p in &ports { + if let Some(id) = p.port_settings_id { + let config = c + .networking_switch_port_settings_view() + .port(id) + .send() + .await? + .into_inner(); + + println!( + "{}{}{}", + p.switch_location.to_string().blue(), + "/".dimmed(), + p.port_name.blue(), + ); + + println!( + "{}", + "=".repeat(p.port_name.len() + p.switch_location.to_string().len() + 1) + .dimmed() + ); + + writeln!( + &mut tw, + "{}\t{}\t{}", + "Autoneg".dimmed(), + "Fec".dimmed(), + "Speed".dimmed(), + )?; + + for l in &config.links { + writeln!(&mut tw, "{:?}\t{:?}\t{:?}", l.autoneg, l.fec, l.speed,)?; + } + tw.flush()?; + println!(""); + + writeln!(&mut tw, "{}\t{}", "Address".dimmed(), "Lot".dimmed())?; + for a in &config.addresses { + let addr = match &a.address { + oxide::types::IpNet::V4(a) => a.to_string(), + oxide::types::IpNet::V6(a) => a.to_string(), + }; + + let alb = addr_lot_blocks + .iter() + .find(|x| x.1.id == a.address_lot_block_id) + .unwrap(); + + writeln!(&mut tw, "{}\t{}", addr, *alb.0.name)?; + } + tw.flush()?; + println!(""); + + writeln!( + &mut tw, + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + "BGP Peer".dimmed(), + "Export".dimmed(), + "Import".dimmed(), + "Communities".dimmed(), + "Connect Retry".dimmed(), + "Delay Open".dimmed(), + "Enforce First AS".dimmed(), + "Hold Time".dimmed(), + "Idle Hold Time".dimmed(), + "Keepalive".dimmed(), + "Local Pref".dimmed(), + "Md5 Auth".dimmed(), + "Min TTL".dimmed(), + "MED".dimmed(), + "Remote ASN".dimmed(), + "VLAN".dimmed(), + )?; + for p in &config.bgp_peers { + writeln!( + &mut tw, + "{}\t{:?}\t{:?}\t{:?}\t{}\t{}\t{}\t{}\t{}\t{}\t{:?}\t{:?}\t{:?}\t{:?}\t{:?}\t{:?}", + p.addr, + p.allowed_export, + p.allowed_import, + p.communities, + p.connect_retry, + p.delay_open, + p.enforce_first_as, + p.hold_time, + p.idle_hold_time, + p.keepalive, + p.local_pref, + p.md5_auth_key, + p.min_ttl, + p.multi_exit_discriminator, + p.remote_asn, + p.vlan_id, + )?; + } + tw.flush()?; + println!(""); + + // Uncomment to see full payload + //println!(""); + //println!("{:#?}", config); + //println!(""); + } + } + + Ok(()) + } +} diff --git a/cli/src/cmd_net/port_status.rs b/cli/src/cmd_net/port_status.rs new file mode 100644 index 00000000..9dafe0f2 --- /dev/null +++ b/cli/src/cmd_net/port_status.rs @@ -0,0 +1,202 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::RunnableCmd; +use anyhow::Result; +use async_trait::async_trait; +use clap::Parser; +use colored::*; +use oxide::context::Context; +use oxide::types::SwitchPort; +use oxide::{Client, ClientSystemHardwareExt}; +use serde::{Deserialize, Serialize}; +use std::io::Write; +use tabwriter::TabWriter; + +/// Get the status of switch ports. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "net port status")] +pub struct CmdPortStatus {} + +#[async_trait] +impl RunnableCmd for CmdPortStatus { + async fn run(&self, ctx: &Context) -> Result<()> { + let c = ctx.client()?; + + let ports = c + .networking_switch_port_list() + .limit(u32::MAX) + .send() + .await? + .into_inner() + .items; + + let (mut sw0, mut sw1): (Vec<&SwitchPort>, Vec<&SwitchPort>) = ports + .iter() + .partition(|x| x.switch_location.as_str() == "switch0"); + + sw0.sort_by_key(|x| x.port_name.as_str()); + sw1.sort_by_key(|x| x.port_name.as_str()); + + println!("{}", "switch0".dimmed()); + println!("{}", "=======".dimmed()); + self.show_switch(&c, "switch0", &sw0).await?; + + println!("{}", "switch1".dimmed()); + println!("{}", "=======".dimmed()); + self.show_switch(&c, "switch1", &sw1).await?; + + Ok(()) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct MacAddr { + a: [u8; 6], +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct LinkStatus { + address: MacAddr, + enabled: bool, + autoneg: bool, + fec: String, + link_state: String, + fsm_state: String, + media: String, + speed: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "snake_case")] +pub enum ReceiverPower { + /// The measurement is represents average optical power, in mW. + Average(f32), + + /// The measurement represents a peak-to-peak, in mW. + PeakToPeak(f32), +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct Monitors { + receiver_power: Vec, + transmitter_bias_current: Vec, + transmitter_power: Vec, +} + +impl CmdPortStatus { + async fn show_switch(&self, c: &Client, sw: &str, ports: &Vec<&SwitchPort>) -> Result<()> { + let mut ltw = TabWriter::new(std::io::stdout()).ansi(true); + let mut mtw = TabWriter::new(std::io::stdout()).ansi(true); + + writeln!( + &mut ltw, + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + "Port".dimmed(), + "Configured".dimmed(), + "Enabled".dimmed(), + "MAC".dimmed(), + "Autoneg".dimmed(), + "FEC".dimmed(), + "Link/FSM State".dimmed(), + "Media".dimmed(), + "Speed".dimmed(), + )?; + + writeln!( + &mut mtw, + "{}\t{}\t{}", + "Receiver Power".dimmed(), + "Transmitter Bias Current".dimmed(), + "Transmitter Power".dimmed(), + )?; + + for p in ports { + let status = c + .networking_switch_port_status() + .port(&p.port_name) + .rack_id(p.rack_id) + .switch_location(sw) + .send() + .await + .ok() + .map(|x| x.into_inner().0); + + //println!("{:?}", status); + + let link = status.as_ref().map(|x| { + let ls: LinkStatus = + serde_json::from_value(x.get("link").unwrap().clone()).unwrap(); + ls + }); + + writeln!( + &mut ltw, + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + p.port_name, + p.port_settings_id.is_some(), + link.as_ref() + .map(|x| x.enabled.to_string()) + .unwrap_or("-".to_string()), + link.as_ref() + .map(|x| format!( + "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", + x.address.a[0], + x.address.a[1], + x.address.a[2], + x.address.a[3], + x.address.a[4], + x.address.a[5] + )) + .unwrap_or("-".to_string()), + link.as_ref() + .map(|x| x.autoneg.to_string()) + .unwrap_or("-".to_string()), + link.as_ref() + .map(|x| x.fec.clone()) + .unwrap_or("-".to_string()), + link.as_ref() + .map(|x| format!("{}/{}", x.link_state, x.fsm_state)) + .unwrap_or("-".to_string()), + link.as_ref() + .map(|x| x.media.clone()) + .unwrap_or("-".to_string()), + link.as_ref() + .map(|x| x.speed.clone()) + .unwrap_or("-".to_string()), + )?; + + let monitors = status.as_ref().map(|x| { + let ls: Monitors = + serde_json::from_value(x.get("monitors").unwrap().clone()).unwrap(); + ls + }); + + writeln!( + &mut mtw, + "{}\t{}\t{}", + monitors + .as_ref() + .map(|x| format!("{:?}", x.receiver_power)) + .unwrap_or("-".to_string()), + monitors + .as_ref() + .map(|x| format!("{:?}", x.transmitter_bias_current)) + .unwrap_or("-".to_string()), + monitors + .as_ref() + .map(|x| format!("{:?}", x.transmitter_power)) + .unwrap_or("-".to_string()), + )?; + } + + ltw.flush()?; + println!(); + mtw.flush()?; + println!(); + + Ok(()) + } +} diff --git a/cli/src/main.rs b/cli/src/main.rs index b1a471ff..92ff7560 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -23,6 +23,7 @@ mod cmd_completion; mod cmd_disk; mod cmd_docs; mod cmd_instance; +mod cmd_net; mod cmd_timeseries; mod cmd_version; @@ -41,7 +42,7 @@ pub trait RunnableCmd: Send + Sync { } pub fn make_cli() -> NewCli<'static> { - NewCli::default() + let cli = NewCli::default() .add_custom::("auth") .add_custom::("api") .add_custom::("docs") @@ -50,7 +51,8 @@ pub fn make_cli() -> NewCli<'static> { .add_custom::("instance serial") .add_custom::("instance from-image") .add_custom::("completion") - .add_custom::("experimental timeseries dashboard") + .add_custom::("experimental timeseries dashboard"); + cmd_net::make_cli(cli) } #[tokio::main]