From 3c17df688366f5334c3d3e41146b575b9c6bfdfe Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Sat, 30 Dec 2023 10:01:28 +0000 Subject: [PATCH 01/21] first cut at "omdb mgs sensors" --- Cargo.lock | 10 + Cargo.toml | 1 + dev-tools/omdb/Cargo.toml | 1 + dev-tools/omdb/src/bin/omdb/mgs.rs | 333 +++++++++++++++++++++++++++++ 4 files changed, 345 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 3cdf3dd678..e4d1a936a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4024,6 +4024,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +dependencies = [ + "serde", +] + [[package]] name = "nanorand" version = "0.7.0" @@ -4863,6 +4872,7 @@ dependencies = [ "humantime", "internal-dns", "ipnetwork", + "multimap", "nexus-client", "nexus-db-model", "nexus-db-queries", diff --git a/Cargo.toml b/Cargo.toml index d4f81b0310..e1f8283d05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -237,6 +237,7 @@ mime_guess = "2.0.4" mockall = "0.12" newtype_derive = "0.1.6" mg-admin-client = { path = "clients/mg-admin-client" } +multimap = "0.8.1" nexus-client = { path = "clients/nexus-client" } nexus-db-model = { path = "nexus/db-model" } nexus-db-queries = { path = "nexus/db-queries" } diff --git a/dev-tools/omdb/Cargo.toml b/dev-tools/omdb/Cargo.toml index 7544374906..b137e049ac 100644 --- a/dev-tools/omdb/Cargo.toml +++ b/dev-tools/omdb/Cargo.toml @@ -42,6 +42,7 @@ uuid.workspace = true ipnetwork.workspace = true omicron-workspace-hack.workspace = true nexus-test-utils.workspace = true +multimap.workspace = true [dev-dependencies] expectorate.workspace = true diff --git a/dev-tools/omdb/src/bin/omdb/mgs.rs b/dev-tools/omdb/src/bin/omdb/mgs.rs index 770cba9f62..fdad55c986 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs.rs @@ -9,10 +9,13 @@ use anyhow::Context; use clap::Args; use clap::Subcommand; use futures::StreamExt; +use gateway_client::types::MeasurementErrorCode; +use gateway_client::types::MeasurementKind; use gateway_client::types::PowerState; use gateway_client::types::RotSlot; use gateway_client::types::RotState; use gateway_client::types::SpComponentCaboose; +use gateway_client::types::SpComponentDetails; use gateway_client::types::SpComponentInfo; use gateway_client::types::SpIdentifier; use gateway_client::types::SpIgnition; @@ -20,8 +23,11 @@ use gateway_client::types::SpIgnitionInfo; use gateway_client::types::SpIgnitionSystemType; use gateway_client::types::SpState; use gateway_client::types::SpType; +use multimap::MultiMap; use tabled::Tabled; +use std::collections::HashMap; + /// Arguments to the "omdb mgs" subcommand #[derive(Debug, Args)] pub struct MgsArgs { @@ -35,13 +41,37 @@ pub struct MgsArgs { #[derive(Debug, Subcommand)] enum MgsCommands { + /// Dashboard of SPs + Dashboard(DashboardArgs), + /// Show information about devices and components visible to MGS Inventory(InventoryArgs), + + /// Show information about sensors, as gleaned by MGS + Sensors(SensorsArgs), } +#[derive(Debug, Args)] +struct DashboardArgs {} + #[derive(Debug, Args)] struct InventoryArgs {} +#[derive(Debug, Args)] +struct SensorsArgs { + /// verbose messages + #[clap(long, short)] + verbose: bool, + + /// restrict to specified sled(s) + #[clap(long, short, use_value_delimiter = true)] + sled: Vec, + + /// exclude specified targets rather than include them + #[clap(long, short, requires = "sled")] + exclude: bool, +} + impl MgsArgs { pub(crate) async fn run_cmd( &self, @@ -71,13 +101,29 @@ impl MgsArgs { let mgs_client = gateway_client::Client::new(&mgs_url, log.clone()); match &self.command { + MgsCommands::Dashboard(dashboard_args) => { + cmd_mgs_dashboard(&mgs_client, dashboard_args).await + } MgsCommands::Inventory(inventory_args) => { cmd_mgs_inventory(&mgs_client, inventory_args).await } + MgsCommands::Sensors(sensors_args) => { + cmd_mgs_sensors(&mgs_client, sensors_args).await + } } } } +/// +/// Runs `omdb mgs dashboard` +/// +async fn cmd_mgs_dashboard( + _mgs_client: &gateway_client::Client, + _args: &DashboardArgs, +) -> Result<(), anyhow::Error> { + anyhow::bail!("not yet"); +} + /// Runs `omdb mgs inventory` /// /// Shows devices and components that are visible to an MGS instance. @@ -148,6 +194,293 @@ async fn cmd_mgs_inventory( Ok(()) } +#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +struct Sensor { + name: String, + kind: MeasurementKind, +} + +impl Sensor { + fn units(&self) -> &str { + match self.kind { + MeasurementKind::Temperature => "°C", + MeasurementKind::Current | MeasurementKind::InputCurrent => "A", + MeasurementKind::Voltage | MeasurementKind::InputVoltage => "V", + MeasurementKind::Speed => "RPM", + MeasurementKind::Power => "W", + } + } + + fn format(&self, value: f32) -> String { + match self.kind { + MeasurementKind::Speed => { + format!("{value:0} RPM") + } + _ => { + format!("{value:.2}{}", self.units()) + } + } + } +} + +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +struct SensorId(u32); + +#[derive(Debug)] +struct SensorMetadata { + sensors_by_sensor: MultiMap, + sensors_by_sensor_and_sp: HashMap<(Sensor, SpIdentifier), SensorId>, + sensors_by_id: HashMap, + sensors_by_sp: MultiMap, + work_by_sp: HashMap)>>, +} + +struct SensorValues(HashMap>); + +struct SpInfo { + devices: MultiMap)>, + timestamps: Vec, +} + +async fn sp_info( + mgs_client: gateway_client::Client, + type_: SpType, + slot: u32, +) -> Result { + let mut devices = MultiMap::new(); + let mut timestamps = vec![]; + + timestamps.push(std::time::Instant::now()); + + // + // First, get a component list. + // + let components = mgs_client.sp_component_list(type_, slot).await?; + timestamps.push(std::time::Instant::now()); + + // + // Now, for every component, we're going to get its details: for those + // that are sensors (and contain measurements), we will store the name + // of the sensor as well as the retrieved value. + // + for c in &components.components { + for s in mgs_client + .sp_component_get(type_, slot, &c.component) + .await? + .iter() + .filter_map(|detail| match detail { + SpComponentDetails::Measurement { kind, name, value } => Some( + (Sensor { name: name.clone(), kind: *kind }, Some(*value)), + ), + SpComponentDetails::MeasurementError { kind, name, error } => { + match error { + MeasurementErrorCode::NoReading + | MeasurementErrorCode::NotPresent => None, + _ => Some(( + Sensor { name: name.clone(), kind: *kind }, + None, + )), + } + } + _ => None, + }) + { + devices.insert(c.component.clone(), s); + } + } + + timestamps.push(std::time::Instant::now()); + + Ok(SpInfo { devices, timestamps }) +} + +async fn sensor_metadata( + mgs_client: &gateway_client::Client, + args: &SensorsArgs, +) -> Result<(SensorMetadata, SensorValues), anyhow::Error> { + // + // First, get all of the SPs that we can see via Ignition + // + let all_sp_list = + mgs_client.ignition_list().await.context("listing ignition")?; + + let mut sp_list = all_sp_list + .iter() + .filter_map(|ignition| { + if matches!(ignition.details, SpIgnition::Yes { .. }) { + let matched = if args.sled.len() > 0 { + if ignition.id.type_ == SpType::Sled { + args.sled + .iter() + .find(|&&v| v == ignition.id.slot) + .is_some() + } else { + false + } + } else { + true + }; + + match (matched, args.exclude) { + (true, true) | (false, false) => None, + _ => Some(ignition.id), + } + } else { + None + } + }) + .collect::>(); + + sp_list.sort(); + + let now = std::time::Instant::now(); + + let mut handles = vec![]; + for sp_id in sp_list { + let mgs_client = mgs_client.clone(); + let type_ = sp_id.type_; + let slot = sp_id.slot; + + let handle = + tokio::spawn(async move { sp_info(mgs_client, type_, slot).await }); + + handles.push((sp_id, handle)); + } + + let mut sensors_by_sensor = MultiMap::new(); + let mut sensors_by_sensor_and_sp = HashMap::new(); + let mut sensors_by_id = HashMap::new(); + let mut sensors_by_sp = MultiMap::new(); + let mut all_values = HashMap::new(); + let mut work_by_sp = HashMap::new(); + + let mut current = 0; + + for (sp_id, handle) in handles { + let mut sp_work = vec![]; + + match handle.await.unwrap() { + Ok(info) => { + for (device, sensors) in info.devices { + let mut device_work = vec![]; + + for (sensor, value) in sensors { + let id = SensorId(current); + current += 1; + + sensors_by_id.insert( + id, + (sp_id, sensor.clone(), device.clone()), + ); + + if value.is_none() && args.verbose { + eprintln!( + "{sp_id:?}: error on {sensor:?} ({device})" + ); + } + + sensors_by_sensor.insert(sensor.clone(), id); + sensors_by_sensor_and_sp.insert((sensor, sp_id), id); + sensors_by_sp.insert(sp_id, id); + all_values.insert(id, value); + + device_work.push(id); + } + + sp_work.push((device, device_work)); + } + + if args.verbose { + eprintln!( + "{:?} {:?} {:?}", + sp_id, + info.timestamps[2].duration_since(info.timestamps[1]), + info.timestamps[1].duration_since(info.timestamps[0]) + ); + } + } + Err(err) => { + eprintln!("failed to read devices for {:?}: {:?}", sp_id, err); + } + } + + work_by_sp.insert(sp_id, sp_work); + } + + if args.verbose { + eprintln!("total discovery time {:?}", now.elapsed()); + } + + Ok(( + SensorMetadata { + sensors_by_sensor, + sensors_by_sensor_and_sp, + sensors_by_id, + sensors_by_sp, + work_by_sp, + }, + SensorValues(all_values), + )) +} + +/// +/// Runs `omdb mgs sensors` +/// +async fn cmd_mgs_sensors( + mgs_client: &gateway_client::Client, + args: &SensorsArgs, +) -> Result<(), anyhow::Error> { + let (metadata, values) = sensor_metadata(mgs_client, args).await?; + + let mut sensors = metadata.sensors_by_sensor.keys().collect::>(); + sensors.sort(); + + let mut sps = metadata.sensors_by_sp.keys().collect::>(); + sps.sort(); + + print!("{:20} ", "NAME"); + + for sp in &sps { + print!( + " {:>8}", + format!("{}-{}", sp_type_to_str(&sp.type_).to_uppercase(), sp.slot) + ); + } + + println!(); + + for sensor in sensors { + print!("{:20} ", sensor.name); + + for sp in &sps { + let lookup = sensor.clone(); + + if let Some(id) = + metadata.sensors_by_sensor_and_sp.get(&(lookup, **sp)) + { + if let Some(value) = values.0.get(id) { + match value { + Some(value) => { + print!(" {:>8}", sensor.format(*value)) + } + None => { + print!(" {:>8}", "X"); + } + } + } else { + print!(" {:>8}", "?"); + } + } else { + print!(" {:>8}", "-"); + } + } + + println!(); + } + + Ok(()) +} + fn sp_type_to_str(s: &SpType) -> &'static str { match s { SpType::Sled => "Sled", From 14c7978d14344b96738a29ad9f04dc25b604c46e Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Sun, 31 Dec 2023 21:20:23 +0000 Subject: [PATCH 02/21] wip --- dev-tools/omdb/src/bin/omdb/mgs.rs | 197 ++++++++++++++++++++++------- 1 file changed, 153 insertions(+), 44 deletions(-) diff --git a/dev-tools/omdb/src/bin/omdb/mgs.rs b/dev-tools/omdb/src/bin/omdb/mgs.rs index fdad55c986..b8dcfa0ef0 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs.rs @@ -64,12 +64,24 @@ struct SensorsArgs { verbose: bool, /// restrict to specified sled(s) - #[clap(long, short, use_value_delimiter = true)] + #[clap(long, use_value_delimiter = true)] sled: Vec, - /// exclude specified targets rather than include them - #[clap(long, short, requires = "sled")] + /// exclude sleds rather than include them + #[clap(long, short)] exclude: bool, + + /// include switches + #[clap(long)] + switches: bool, + + /// include PSC + #[clap(long)] + psc: bool, + + /// sleep + #[clap(long, short)] + sleep: bool, } impl MgsArgs { @@ -308,29 +320,34 @@ async fn sensor_metadata( .iter() .filter_map(|ignition| { if matches!(ignition.details, SpIgnition::Yes { .. }) { - let matched = if args.sled.len() > 0 { - if ignition.id.type_ == SpType::Sled { + if ignition.id.type_ == SpType::Sled { + let matched = if args.sled.len() > 0 { args.sled .iter() .find(|&&v| v == ignition.id.slot) .is_some() } else { - false - } - } else { - true - }; + true + }; - match (matched, args.exclude) { - (true, true) | (false, false) => None, - _ => Some(ignition.id), + if matched != args.exclude { + return Some(ignition.id); + } } - } else { - None } + None }) .collect::>(); + if args.switches { + sp_list.push(SpIdentifier { type_: SpType::Switch, slot: 0 }); + sp_list.push(SpIdentifier { type_: SpType::Switch, slot: 1 }); + } + + if args.psc { + sp_list.push(SpIdentifier { type_: SpType::Power, slot: 0 }); + } + sp_list.sort(); let now = std::time::Instant::now(); @@ -423,6 +440,71 @@ async fn sensor_metadata( )) } +async fn sp_read_sensors( + mgs_client: &gateway_client::Client, + id: &SpIdentifier, + metadata: std::sync::Arc<&SensorMetadata>, +) -> Result)>, anyhow::Error> { + let work = metadata.work_by_sp.get(id).unwrap(); + let mut rval = vec![]; + + for (component, ids) in work.iter() { + for (value, id) in mgs_client + .sp_component_get(id.type_, id.slot, component) + .await? + .iter() + .filter_map(|detail| match detail { + SpComponentDetails::Measurement { kind: _, name: _, value } => { + Some(Some(*value)) + } + SpComponentDetails::MeasurementError { kind, name, error } => { + match error { + MeasurementErrorCode::NoReading + | MeasurementErrorCode::NotPresent => None, + _ => Some(None), + } + } + _ => None, + }) + .zip(ids.iter()) + { + rval.push((*id, value)); + } + } + + Ok(rval) +} + +async fn sensor_data( + mgs_client: &gateway_client::Client, + metadata: std::sync::Arc<&'static SensorMetadata>, +) -> Result { + let mut all_values = HashMap::new(); + let mut handles = vec![]; + + for sp_id in metadata.sensors_by_sp.keys() { + let mgs_client = mgs_client.clone(); + let id = *sp_id; + let metadata = metadata.clone(); + + let handle = tokio::spawn(async move { + sp_read_sensors(&mgs_client, &id, metadata).await + }); + + handles.push((sp_id, handle)); + } + + for (sp_id, handle) in handles { + let rval = handle.await.unwrap()?; + + for (id, value) in rval { + all_values.insert(id, value); + } + } + + Ok(SensorValues(all_values)) +} + /// /// Runs `omdb mgs sensors` /// @@ -430,7 +512,15 @@ async fn cmd_mgs_sensors( mgs_client: &gateway_client::Client, args: &SensorsArgs, ) -> Result<(), anyhow::Error> { - let (metadata, values) = sensor_metadata(mgs_client, args).await?; + let (metadata, mut values) = sensor_metadata(mgs_client, args).await?; + + // + // A bit of shenangians to force metadata to be 'static -- which allows + // us to share it with tasks. + // + let metadata = Box::leak(Box::new(metadata)); + let metadata: &_ = metadata; + let metadata = std::sync::Arc::new(metadata); let mut sensors = metadata.sensors_by_sensor.keys().collect::>(); sensors.sort(); @@ -438,44 +528,63 @@ async fn cmd_mgs_sensors( let mut sps = metadata.sensors_by_sp.keys().collect::>(); sps.sort(); - print!("{:20} ", "NAME"); + loop { + print!("{:20} ", "NAME"); - for sp in &sps { - print!( - " {:>8}", - format!("{}-{}", sp_type_to_str(&sp.type_).to_uppercase(), sp.slot) - ); - } - - println!(); + for sp in &sps { + print!( + " {:>8}", + format!( + "{}-{}", + sp_type_to_str(&sp.type_).to_uppercase(), + sp.slot + ) + ); + } - for sensor in sensors { - print!("{:20} ", sensor.name); + println!(); - for sp in &sps { - let lookup = sensor.clone(); - - if let Some(id) = - metadata.sensors_by_sensor_and_sp.get(&(lookup, **sp)) - { - if let Some(value) = values.0.get(id) { - match value { - Some(value) => { - print!(" {:>8}", sensor.format(*value)) - } - None => { - print!(" {:>8}", "X"); + for sensor in &sensors { + print!("{:20} ", sensor.name); + + for sp in &sps { + let lookup = sensor.clone(); + + if let Some(id) = metadata + .sensors_by_sensor_and_sp + .get(&(lookup.clone(), **sp)) + { + if let Some(value) = values.0.get(id) { + match value { + Some(value) => { + print!(" {:>8}", sensor.format(*value)) + } + None => { + print!(" {:>8}", "X"); + } } + } else { + print!(" {:>8}", "?"); } } else { - print!(" {:>8}", "?"); + print!(" {:>8}", "-"); } - } else { - print!(" {:>8}", "-"); } + + println!(); } - println!(); + if !args.sleep { + break; + } + + tokio::time::sleep_until( + tokio::time::Instant::now() + + tokio::time::Duration::from_millis(1000), + ) + .await; + + values = sensor_data(mgs_client, metadata.clone()).await?; } Ok(()) From 6ca8fc464732b5a430fb872d6a2ac2fdca054c0c Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Mon, 1 Jan 2024 06:52:41 +0000 Subject: [PATCH 03/21] wip --- dev-tools/omdb/src/bin/omdb/mgs.rs | 219 ++++++++++++++++++++++------- 1 file changed, 170 insertions(+), 49 deletions(-) diff --git a/dev-tools/omdb/src/bin/omdb/mgs.rs b/dev-tools/omdb/src/bin/omdb/mgs.rs index b8dcfa0ef0..746b2cd422 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs.rs @@ -5,7 +5,7 @@ //! Prototype code for collecting information from systems in the rack use crate::Omdb; -use anyhow::Context; +use anyhow::{bail, Context}; use clap::Args; use clap::Subcommand; use futures::StreamExt; @@ -26,7 +26,8 @@ use gateway_client::types::SpType; use multimap::MultiMap; use tabled::Tabled; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; +use std::time::{Duration, SystemTime}; /// Arguments to the "omdb mgs" subcommand #[derive(Debug, Args)] @@ -65,7 +66,7 @@ struct SensorsArgs { /// restrict to specified sled(s) #[clap(long, use_value_delimiter = true)] - sled: Vec, + sleds: Vec, /// exclude sleds rather than include them #[clap(long, short)] @@ -79,9 +80,31 @@ struct SensorsArgs { #[clap(long)] psc: bool, - /// sleep + /// print sensors every second #[clap(long, short)] sleep: bool, + + /// parseable output + #[clap(long, short)] + parseable: bool, + + /// restrict sensors by type of sensor + #[clap( + long, + short, + value_name = "sensor type", + use_value_delimiter = true + )] + types: Option>, + + /// restrict sensors by name + #[clap( + long, + short, + value_name = "sensor name", + use_value_delimiter = true + )] + named: Option>, } impl MgsArgs { @@ -223,16 +246,51 @@ impl Sensor { } } - fn format(&self, value: f32) -> String { - match self.kind { - MeasurementKind::Speed => { - format!("{value:0} RPM") - } - _ => { - format!("{value:.2}{}", self.units()) + fn format(&self, value: f32, parseable: bool) -> String { + if parseable { + format!("{value}") + } else { + match self.kind { + MeasurementKind::Speed => { + format!("{value:0} RPM") + } + _ => { + format!("{value:.2}{}", self.units()) + } } } } + + pub fn to_kind_string(&self) -> &str { + match self.kind { + MeasurementKind::Temperature => "temp", + MeasurementKind::Power => "power", + MeasurementKind::Current => "current", + MeasurementKind::Voltage => "voltage", + MeasurementKind::InputCurrent => "input-current", + MeasurementKind::InputVoltage => "input-voltage", + MeasurementKind::Speed => "speed", + } + } + + pub fn from_string(name: &str, kind: &str) -> Option { + let k = match kind { + "temp" | "temperature" => Some(MeasurementKind::Temperature), + "power" => Some(MeasurementKind::Power), + "current" => Some(MeasurementKind::Current), + "voltage" => Some(MeasurementKind::Voltage), + "input-current" => Some(MeasurementKind::InputCurrent), + "input-voltage" => Some(MeasurementKind::InputVoltage), + "speed" => Some(MeasurementKind::Speed), + _ => None, + }; + + if let Some(kind) = k { + Some(Sensor { name: name.to_string(), kind }) + } else { + None + } + } } #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] @@ -241,7 +299,7 @@ struct SensorId(u32); #[derive(Debug)] struct SensorMetadata { sensors_by_sensor: MultiMap, - sensors_by_sensor_and_sp: HashMap<(Sensor, SpIdentifier), SensorId>, + sensors_by_sensor_and_sp: HashMap>, sensors_by_id: HashMap, sensors_by_sp: MultiMap, work_by_sp: HashMap)>>, @@ -310,6 +368,27 @@ async fn sensor_metadata( mgs_client: &gateway_client::Client, args: &SensorsArgs, ) -> Result<(SensorMetadata, SensorValues), anyhow::Error> { + let by_kind = if let Some(types) = &args.types { + let mut h = HashSet::new(); + + for t in types { + h.insert(match Sensor::from_string("", &t) { + None => bail!("invalid sensor kind {t}"), + Some(s) => s.kind, + }); + } + + Some(h) + } else { + None + }; + + let by_name = if let Some(named) = &args.named { + Some(named.into_iter().collect::>()) + } else { + None + }; + // // First, get all of the SPs that we can see via Ignition // @@ -321,8 +400,8 @@ async fn sensor_metadata( .filter_map(|ignition| { if matches!(ignition.details, SpIgnition::Yes { .. }) { if ignition.id.type_ == SpType::Sled { - let matched = if args.sled.len() > 0 { - args.sled + let matched = if args.sleds.len() > 0 { + args.sleds .iter() .find(|&&v| v == ignition.id.slot) .is_some() @@ -382,6 +461,18 @@ async fn sensor_metadata( let mut device_work = vec![]; for (sensor, value) in sensors { + if let Some(ref by_kind) = by_kind { + if by_kind.get(&sensor.kind).is_none() { + continue; + } + } + + if let Some(ref by_name) = by_name { + if by_name.get(&sensor.name).is_none() { + continue; + } + } + let id = SensorId(current); current += 1; @@ -392,12 +483,16 @@ async fn sensor_metadata( if value.is_none() && args.verbose { eprintln!( - "{sp_id:?}: error on {sensor:?} ({device})" + "mgs: error for {sp_id:?} on {sensor:?} ({device})" ); } sensors_by_sensor.insert(sensor.clone(), id); - sensors_by_sensor_and_sp.insert((sensor, sp_id), id); + + let by_sp = sensors_by_sensor_and_sp + .entry(sensor) + .or_insert_with(|| HashMap::new()); + by_sp.insert(sp_id, id); sensors_by_sp.insert(sp_id, id); all_values.insert(id, value); @@ -409,8 +504,7 @@ async fn sensor_metadata( if args.verbose { eprintln!( - "{:?} {:?} {:?}", - sp_id, + "mgs: latencies for {sp_id:?}: {:.1?} {:.1?}", info.timestamps[2].duration_since(info.timestamps[1]), info.timestamps[1].duration_since(info.timestamps[0]) ); @@ -457,7 +551,7 @@ async fn sp_read_sensors( SpComponentDetails::Measurement { kind: _, name: _, value } => { Some(Some(*value)) } - SpComponentDetails::MeasurementError { kind, name, error } => { + SpComponentDetails::MeasurementError { error, .. } => { match error { MeasurementErrorCode::NoReading | MeasurementErrorCode::NotPresent => None, @@ -491,10 +585,10 @@ async fn sensor_data( sp_read_sensors(&mgs_client, &id, metadata).await }); - handles.push((sp_id, handle)); + handles.push(handle); } - for (sp_id, handle) in handles { + for handle in handles { let rval = handle.await.unwrap()?; for (id, value) in rval { @@ -528,47 +622,73 @@ async fn cmd_mgs_sensors( let mut sps = metadata.sensors_by_sp.keys().collect::>(); sps.sort(); - loop { - print!("{:20} ", "NAME"); + let print_value = |v| { + if args.parseable { + print!(",{v}"); + } else { + print!(" {v:>8}"); + } + }; + + let print_header = || { + if !args.parseable { + print!("{:20} ", "NAME"); + } else { + print!("TIME,SENSOR"); + } for sp in &sps { + print_value(format!( + "{}-{}", + sp_type_to_str(&sp.type_).to_uppercase(), + sp.slot + )); + } + + println!(); + }; + + let print_name = |sensor: &Sensor, now: Duration| { + if !args.parseable { + print!("{:20} ", sensor.name); + } else { print!( - " {:>8}", - format!( - "{}-{}", - sp_type_to_str(&sp.type_).to_uppercase(), - sp.slot - ) + "{},{},{}", + now.as_secs(), + sensor.name, + sensor.to_kind_string() ); } + }; - println!(); + let mut wakeup = + tokio::time::Instant::now() + tokio::time::Duration::from_millis(1000); + + print_header(); + + loop { + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; for sensor in &sensors { - print!("{:20} ", sensor.name); + print_name(sensor, now); - for sp in &sps { - let lookup = sensor.clone(); + let by_sp = metadata.sensors_by_sensor_and_sp.get(sensor).unwrap(); - if let Some(id) = metadata - .sensors_by_sensor_and_sp - .get(&(lookup.clone(), **sp)) - { + for sp in &sps { + print_value(if let Some(id) = by_sp.get(sp) { if let Some(value) = values.0.get(id) { match value { Some(value) => { - print!(" {:>8}", sensor.format(*value)) - } - None => { - print!(" {:>8}", "X"); + sensor.format(*value, args.parseable) } + None => "X".to_string(), } } else { - print!(" {:>8}", "?"); + "?".to_string() } } else { - print!(" {:>8}", "-"); - } + "-".to_string() + }); } println!(); @@ -578,13 +698,14 @@ async fn cmd_mgs_sensors( break; } - tokio::time::sleep_until( - tokio::time::Instant::now() - + tokio::time::Duration::from_millis(1000), - ) - .await; + tokio::time::sleep_until(wakeup).await; + wakeup += tokio::time::Duration::from_millis(1000); values = sensor_data(mgs_client, metadata.clone()).await?; + + if !args.parseable { + print_header(); + } } Ok(()) From d7650127bd805d19755b91cf0cea8224d70672c0 Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Mon, 1 Jan 2024 19:38:58 +0000 Subject: [PATCH 04/21] refactor --- dev-tools/omdb/src/bin/omdb/mgs.rs | 561 +------------------ dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs | 24 + dev-tools/omdb/src/bin/omdb/mgs/sensors.rs | 548 ++++++++++++++++++ 3 files changed, 580 insertions(+), 553 deletions(-) create mode 100644 dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs create mode 100644 dev-tools/omdb/src/bin/omdb/mgs/sensors.rs diff --git a/dev-tools/omdb/src/bin/omdb/mgs.rs b/dev-tools/omdb/src/bin/omdb/mgs.rs index 746b2cd422..c7305b90cb 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs.rs @@ -5,17 +5,14 @@ //! Prototype code for collecting information from systems in the rack use crate::Omdb; -use anyhow::{bail, Context}; +use anyhow::Context; use clap::Args; use clap::Subcommand; use futures::StreamExt; -use gateway_client::types::MeasurementErrorCode; -use gateway_client::types::MeasurementKind; use gateway_client::types::PowerState; use gateway_client::types::RotSlot; use gateway_client::types::RotState; use gateway_client::types::SpComponentCaboose; -use gateway_client::types::SpComponentDetails; use gateway_client::types::SpComponentInfo; use gateway_client::types::SpIdentifier; use gateway_client::types::SpIgnition; @@ -23,11 +20,13 @@ use gateway_client::types::SpIgnitionInfo; use gateway_client::types::SpIgnitionSystemType; use gateway_client::types::SpState; use gateway_client::types::SpType; -use multimap::MultiMap; use tabled::Tabled; -use std::collections::{HashMap, HashSet}; -use std::time::{Duration, SystemTime}; +mod dashboard; +mod sensors; + +use dashboard::DashboardArgs; +use sensors::SensorsArgs; /// Arguments to the "omdb mgs" subcommand #[derive(Debug, Args)] @@ -52,61 +51,9 @@ enum MgsCommands { Sensors(SensorsArgs), } -#[derive(Debug, Args)] -struct DashboardArgs {} - #[derive(Debug, Args)] struct InventoryArgs {} -#[derive(Debug, Args)] -struct SensorsArgs { - /// verbose messages - #[clap(long, short)] - verbose: bool, - - /// restrict to specified sled(s) - #[clap(long, use_value_delimiter = true)] - sleds: Vec, - - /// exclude sleds rather than include them - #[clap(long, short)] - exclude: bool, - - /// include switches - #[clap(long)] - switches: bool, - - /// include PSC - #[clap(long)] - psc: bool, - - /// print sensors every second - #[clap(long, short)] - sleep: bool, - - /// parseable output - #[clap(long, short)] - parseable: bool, - - /// restrict sensors by type of sensor - #[clap( - long, - short, - value_name = "sensor type", - use_value_delimiter = true - )] - types: Option>, - - /// restrict sensors by name - #[clap( - long, - short, - value_name = "sensor name", - use_value_delimiter = true - )] - named: Option>, -} - impl MgsArgs { pub(crate) async fn run_cmd( &self, @@ -137,28 +84,18 @@ impl MgsArgs { match &self.command { MgsCommands::Dashboard(dashboard_args) => { - cmd_mgs_dashboard(&mgs_client, dashboard_args).await + dashboard::cmd_mgs_dashboard(&mgs_client, dashboard_args).await } MgsCommands::Inventory(inventory_args) => { cmd_mgs_inventory(&mgs_client, inventory_args).await } MgsCommands::Sensors(sensors_args) => { - cmd_mgs_sensors(&mgs_client, sensors_args).await + sensors::cmd_mgs_sensors(&mgs_client, sensors_args).await } } } } -/// -/// Runs `omdb mgs dashboard` -/// -async fn cmd_mgs_dashboard( - _mgs_client: &gateway_client::Client, - _args: &DashboardArgs, -) -> Result<(), anyhow::Error> { - anyhow::bail!("not yet"); -} - /// Runs `omdb mgs inventory` /// /// Shows devices and components that are visible to an MGS instance. @@ -229,488 +166,6 @@ async fn cmd_mgs_inventory( Ok(()) } -#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] -struct Sensor { - name: String, - kind: MeasurementKind, -} - -impl Sensor { - fn units(&self) -> &str { - match self.kind { - MeasurementKind::Temperature => "°C", - MeasurementKind::Current | MeasurementKind::InputCurrent => "A", - MeasurementKind::Voltage | MeasurementKind::InputVoltage => "V", - MeasurementKind::Speed => "RPM", - MeasurementKind::Power => "W", - } - } - - fn format(&self, value: f32, parseable: bool) -> String { - if parseable { - format!("{value}") - } else { - match self.kind { - MeasurementKind::Speed => { - format!("{value:0} RPM") - } - _ => { - format!("{value:.2}{}", self.units()) - } - } - } - } - - pub fn to_kind_string(&self) -> &str { - match self.kind { - MeasurementKind::Temperature => "temp", - MeasurementKind::Power => "power", - MeasurementKind::Current => "current", - MeasurementKind::Voltage => "voltage", - MeasurementKind::InputCurrent => "input-current", - MeasurementKind::InputVoltage => "input-voltage", - MeasurementKind::Speed => "speed", - } - } - - pub fn from_string(name: &str, kind: &str) -> Option { - let k = match kind { - "temp" | "temperature" => Some(MeasurementKind::Temperature), - "power" => Some(MeasurementKind::Power), - "current" => Some(MeasurementKind::Current), - "voltage" => Some(MeasurementKind::Voltage), - "input-current" => Some(MeasurementKind::InputCurrent), - "input-voltage" => Some(MeasurementKind::InputVoltage), - "speed" => Some(MeasurementKind::Speed), - _ => None, - }; - - if let Some(kind) = k { - Some(Sensor { name: name.to_string(), kind }) - } else { - None - } - } -} - -#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] -struct SensorId(u32); - -#[derive(Debug)] -struct SensorMetadata { - sensors_by_sensor: MultiMap, - sensors_by_sensor_and_sp: HashMap>, - sensors_by_id: HashMap, - sensors_by_sp: MultiMap, - work_by_sp: HashMap)>>, -} - -struct SensorValues(HashMap>); - -struct SpInfo { - devices: MultiMap)>, - timestamps: Vec, -} - -async fn sp_info( - mgs_client: gateway_client::Client, - type_: SpType, - slot: u32, -) -> Result { - let mut devices = MultiMap::new(); - let mut timestamps = vec![]; - - timestamps.push(std::time::Instant::now()); - - // - // First, get a component list. - // - let components = mgs_client.sp_component_list(type_, slot).await?; - timestamps.push(std::time::Instant::now()); - - // - // Now, for every component, we're going to get its details: for those - // that are sensors (and contain measurements), we will store the name - // of the sensor as well as the retrieved value. - // - for c in &components.components { - for s in mgs_client - .sp_component_get(type_, slot, &c.component) - .await? - .iter() - .filter_map(|detail| match detail { - SpComponentDetails::Measurement { kind, name, value } => Some( - (Sensor { name: name.clone(), kind: *kind }, Some(*value)), - ), - SpComponentDetails::MeasurementError { kind, name, error } => { - match error { - MeasurementErrorCode::NoReading - | MeasurementErrorCode::NotPresent => None, - _ => Some(( - Sensor { name: name.clone(), kind: *kind }, - None, - )), - } - } - _ => None, - }) - { - devices.insert(c.component.clone(), s); - } - } - - timestamps.push(std::time::Instant::now()); - - Ok(SpInfo { devices, timestamps }) -} - -async fn sensor_metadata( - mgs_client: &gateway_client::Client, - args: &SensorsArgs, -) -> Result<(SensorMetadata, SensorValues), anyhow::Error> { - let by_kind = if let Some(types) = &args.types { - let mut h = HashSet::new(); - - for t in types { - h.insert(match Sensor::from_string("", &t) { - None => bail!("invalid sensor kind {t}"), - Some(s) => s.kind, - }); - } - - Some(h) - } else { - None - }; - - let by_name = if let Some(named) = &args.named { - Some(named.into_iter().collect::>()) - } else { - None - }; - - // - // First, get all of the SPs that we can see via Ignition - // - let all_sp_list = - mgs_client.ignition_list().await.context("listing ignition")?; - - let mut sp_list = all_sp_list - .iter() - .filter_map(|ignition| { - if matches!(ignition.details, SpIgnition::Yes { .. }) { - if ignition.id.type_ == SpType::Sled { - let matched = if args.sleds.len() > 0 { - args.sleds - .iter() - .find(|&&v| v == ignition.id.slot) - .is_some() - } else { - true - }; - - if matched != args.exclude { - return Some(ignition.id); - } - } - } - None - }) - .collect::>(); - - if args.switches { - sp_list.push(SpIdentifier { type_: SpType::Switch, slot: 0 }); - sp_list.push(SpIdentifier { type_: SpType::Switch, slot: 1 }); - } - - if args.psc { - sp_list.push(SpIdentifier { type_: SpType::Power, slot: 0 }); - } - - sp_list.sort(); - - let now = std::time::Instant::now(); - - let mut handles = vec![]; - for sp_id in sp_list { - let mgs_client = mgs_client.clone(); - let type_ = sp_id.type_; - let slot = sp_id.slot; - - let handle = - tokio::spawn(async move { sp_info(mgs_client, type_, slot).await }); - - handles.push((sp_id, handle)); - } - - let mut sensors_by_sensor = MultiMap::new(); - let mut sensors_by_sensor_and_sp = HashMap::new(); - let mut sensors_by_id = HashMap::new(); - let mut sensors_by_sp = MultiMap::new(); - let mut all_values = HashMap::new(); - let mut work_by_sp = HashMap::new(); - - let mut current = 0; - - for (sp_id, handle) in handles { - let mut sp_work = vec![]; - - match handle.await.unwrap() { - Ok(info) => { - for (device, sensors) in info.devices { - let mut device_work = vec![]; - - for (sensor, value) in sensors { - if let Some(ref by_kind) = by_kind { - if by_kind.get(&sensor.kind).is_none() { - continue; - } - } - - if let Some(ref by_name) = by_name { - if by_name.get(&sensor.name).is_none() { - continue; - } - } - - let id = SensorId(current); - current += 1; - - sensors_by_id.insert( - id, - (sp_id, sensor.clone(), device.clone()), - ); - - if value.is_none() && args.verbose { - eprintln!( - "mgs: error for {sp_id:?} on {sensor:?} ({device})" - ); - } - - sensors_by_sensor.insert(sensor.clone(), id); - - let by_sp = sensors_by_sensor_and_sp - .entry(sensor) - .or_insert_with(|| HashMap::new()); - by_sp.insert(sp_id, id); - sensors_by_sp.insert(sp_id, id); - all_values.insert(id, value); - - device_work.push(id); - } - - sp_work.push((device, device_work)); - } - - if args.verbose { - eprintln!( - "mgs: latencies for {sp_id:?}: {:.1?} {:.1?}", - info.timestamps[2].duration_since(info.timestamps[1]), - info.timestamps[1].duration_since(info.timestamps[0]) - ); - } - } - Err(err) => { - eprintln!("failed to read devices for {:?}: {:?}", sp_id, err); - } - } - - work_by_sp.insert(sp_id, sp_work); - } - - if args.verbose { - eprintln!("total discovery time {:?}", now.elapsed()); - } - - Ok(( - SensorMetadata { - sensors_by_sensor, - sensors_by_sensor_and_sp, - sensors_by_id, - sensors_by_sp, - work_by_sp, - }, - SensorValues(all_values), - )) -} - -async fn sp_read_sensors( - mgs_client: &gateway_client::Client, - id: &SpIdentifier, - metadata: std::sync::Arc<&SensorMetadata>, -) -> Result)>, anyhow::Error> { - let work = metadata.work_by_sp.get(id).unwrap(); - let mut rval = vec![]; - - for (component, ids) in work.iter() { - for (value, id) in mgs_client - .sp_component_get(id.type_, id.slot, component) - .await? - .iter() - .filter_map(|detail| match detail { - SpComponentDetails::Measurement { kind: _, name: _, value } => { - Some(Some(*value)) - } - SpComponentDetails::MeasurementError { error, .. } => { - match error { - MeasurementErrorCode::NoReading - | MeasurementErrorCode::NotPresent => None, - _ => Some(None), - } - } - _ => None, - }) - .zip(ids.iter()) - { - rval.push((*id, value)); - } - } - - Ok(rval) -} - -async fn sensor_data( - mgs_client: &gateway_client::Client, - metadata: std::sync::Arc<&'static SensorMetadata>, -) -> Result { - let mut all_values = HashMap::new(); - let mut handles = vec![]; - - for sp_id in metadata.sensors_by_sp.keys() { - let mgs_client = mgs_client.clone(); - let id = *sp_id; - let metadata = metadata.clone(); - - let handle = tokio::spawn(async move { - sp_read_sensors(&mgs_client, &id, metadata).await - }); - - handles.push(handle); - } - - for handle in handles { - let rval = handle.await.unwrap()?; - - for (id, value) in rval { - all_values.insert(id, value); - } - } - - Ok(SensorValues(all_values)) -} - -/// -/// Runs `omdb mgs sensors` -/// -async fn cmd_mgs_sensors( - mgs_client: &gateway_client::Client, - args: &SensorsArgs, -) -> Result<(), anyhow::Error> { - let (metadata, mut values) = sensor_metadata(mgs_client, args).await?; - - // - // A bit of shenangians to force metadata to be 'static -- which allows - // us to share it with tasks. - // - let metadata = Box::leak(Box::new(metadata)); - let metadata: &_ = metadata; - let metadata = std::sync::Arc::new(metadata); - - let mut sensors = metadata.sensors_by_sensor.keys().collect::>(); - sensors.sort(); - - let mut sps = metadata.sensors_by_sp.keys().collect::>(); - sps.sort(); - - let print_value = |v| { - if args.parseable { - print!(",{v}"); - } else { - print!(" {v:>8}"); - } - }; - - let print_header = || { - if !args.parseable { - print!("{:20} ", "NAME"); - } else { - print!("TIME,SENSOR"); - } - - for sp in &sps { - print_value(format!( - "{}-{}", - sp_type_to_str(&sp.type_).to_uppercase(), - sp.slot - )); - } - - println!(); - }; - - let print_name = |sensor: &Sensor, now: Duration| { - if !args.parseable { - print!("{:20} ", sensor.name); - } else { - print!( - "{},{},{}", - now.as_secs(), - sensor.name, - sensor.to_kind_string() - ); - } - }; - - let mut wakeup = - tokio::time::Instant::now() + tokio::time::Duration::from_millis(1000); - - print_header(); - - loop { - let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; - - for sensor in &sensors { - print_name(sensor, now); - - let by_sp = metadata.sensors_by_sensor_and_sp.get(sensor).unwrap(); - - for sp in &sps { - print_value(if let Some(id) = by_sp.get(sp) { - if let Some(value) = values.0.get(id) { - match value { - Some(value) => { - sensor.format(*value, args.parseable) - } - None => "X".to_string(), - } - } else { - "?".to_string() - } - } else { - "-".to_string() - }); - } - - println!(); - } - - if !args.sleep { - break; - } - - tokio::time::sleep_until(wakeup).await; - wakeup += tokio::time::Duration::from_millis(1000); - - values = sensor_data(mgs_client, metadata.clone()).await?; - - if !args.parseable { - print_header(); - } - } - - Ok(()) -} - fn sp_type_to_str(s: &SpType) -> &'static str { match s { SpType::Sled => "Sled", diff --git a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs new file mode 100644 index 0000000000..31f8954014 --- /dev/null +++ b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs @@ -0,0 +1,24 @@ +// 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/. + +//! Code for the MGS dashboard subcommand + +use clap::Args; + +#[derive(Debug, Args)] +pub(crate) struct DashboardArgs { + /// simulate using specified file as input + #[clap(long, short)] + input: Option, +} + +/// +/// Runs `omdb mgs dashboard` +/// +pub(crate) async fn cmd_mgs_dashboard( + _mgs_client: &gateway_client::Client, + _args: &DashboardArgs, +) -> Result<(), anyhow::Error> { + anyhow::bail!("not yet"); +} diff --git a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs new file mode 100644 index 0000000000..64419bdff9 --- /dev/null +++ b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs @@ -0,0 +1,548 @@ +// 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/. + +//! Implementation of the "mgs sensors" subcommand + +use anyhow::{bail, Context}; +use clap::Args; +use gateway_client::types::MeasurementErrorCode; +use gateway_client::types::MeasurementKind; +use gateway_client::types::SpComponentDetails; +use gateway_client::types::SpIdentifier; +use gateway_client::types::SpIgnition; +use gateway_client::types::SpType; +use multimap::MultiMap; +use std::collections::{HashMap, HashSet}; +use std::time::{Duration, SystemTime}; + +#[derive(Debug, Args)] +pub(crate) struct SensorsArgs { + /// verbose messages + #[clap(long, short)] + verbose: bool, + + /// restrict to specified sled(s) + #[clap(long, use_value_delimiter = true)] + sleds: Vec, + + /// exclude sleds rather than include them + #[clap(long, short)] + exclude: bool, + + /// include switches + #[clap(long)] + switches: bool, + + /// include PSC + #[clap(long)] + psc: bool, + + /// print sensors every second + #[clap(long, short)] + sleep: bool, + + /// parseable output + #[clap(long, short)] + parseable: bool, + + /// restrict sensors by type of sensor + #[clap( + long, + short, + value_name = "sensor type", + use_value_delimiter = true + )] + types: Option>, + + /// restrict sensors by name + #[clap( + long, + short, + value_name = "sensor name", + use_value_delimiter = true + )] + named: Option>, +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +struct Sensor { + name: String, + kind: MeasurementKind, +} + +impl Sensor { + fn units(&self) -> &str { + match self.kind { + MeasurementKind::Temperature => "°C", + MeasurementKind::Current | MeasurementKind::InputCurrent => "A", + MeasurementKind::Voltage | MeasurementKind::InputVoltage => "V", + MeasurementKind::Speed => "RPM", + MeasurementKind::Power => "W", + } + } + + fn format(&self, value: f32, parseable: bool) -> String { + if parseable { + format!("{value}") + } else { + match self.kind { + MeasurementKind::Speed => { + format!("{value:0} RPM") + } + _ => { + format!("{value:.2}{}", self.units()) + } + } + } + } + + fn to_kind_string(&self) -> &str { + match self.kind { + MeasurementKind::Temperature => "temp", + MeasurementKind::Power => "power", + MeasurementKind::Current => "current", + MeasurementKind::Voltage => "voltage", + MeasurementKind::InputCurrent => "input-current", + MeasurementKind::InputVoltage => "input-voltage", + MeasurementKind::Speed => "speed", + } + } + + fn from_string(name: &str, kind: &str) -> Option { + let k = match kind { + "temp" | "temperature" => Some(MeasurementKind::Temperature), + "power" => Some(MeasurementKind::Power), + "current" => Some(MeasurementKind::Current), + "voltage" => Some(MeasurementKind::Voltage), + "input-current" => Some(MeasurementKind::InputCurrent), + "input-voltage" => Some(MeasurementKind::InputVoltage), + "speed" => Some(MeasurementKind::Speed), + _ => None, + }; + + if let Some(kind) = k { + Some(Sensor { name: name.to_string(), kind }) + } else { + None + } + } +} + +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +struct SensorId(u32); + +#[derive(Debug)] +struct SensorMetadata { + sensors_by_sensor: MultiMap, + sensors_by_sensor_and_sp: HashMap>, + sensors_by_id: HashMap, + sensors_by_sp: MultiMap, + work_by_sp: HashMap)>>, +} + +struct SensorValues(HashMap>); + +struct SpInfo { + devices: MultiMap)>, + timestamps: Vec, +} + +async fn sp_info( + mgs_client: gateway_client::Client, + type_: SpType, + slot: u32, +) -> Result { + let mut devices = MultiMap::new(); + let mut timestamps = vec![]; + + timestamps.push(std::time::Instant::now()); + + // + // First, get a component list. + // + let components = mgs_client.sp_component_list(type_, slot).await?; + timestamps.push(std::time::Instant::now()); + + // + // Now, for every component, we're going to get its details: for those + // that are sensors (and contain measurements), we will store the name + // of the sensor as well as the retrieved value. + // + for c in &components.components { + for s in mgs_client + .sp_component_get(type_, slot, &c.component) + .await? + .iter() + .filter_map(|detail| match detail { + SpComponentDetails::Measurement { kind, name, value } => Some( + (Sensor { name: name.clone(), kind: *kind }, Some(*value)), + ), + SpComponentDetails::MeasurementError { kind, name, error } => { + match error { + MeasurementErrorCode::NoReading + | MeasurementErrorCode::NotPresent => None, + _ => Some(( + Sensor { name: name.clone(), kind: *kind }, + None, + )), + } + } + _ => None, + }) + { + devices.insert(c.component.clone(), s); + } + } + + timestamps.push(std::time::Instant::now()); + + Ok(SpInfo { devices, timestamps }) +} + +async fn sensor_metadata( + mgs_client: &gateway_client::Client, + args: &SensorsArgs, +) -> Result<(SensorMetadata, SensorValues), anyhow::Error> { + let by_kind = if let Some(types) = &args.types { + let mut h = HashSet::new(); + + for t in types { + h.insert(match Sensor::from_string("", &t) { + None => bail!("invalid sensor kind {t}"), + Some(s) => s.kind, + }); + } + + Some(h) + } else { + None + }; + + let by_name = if let Some(named) = &args.named { + Some(named.into_iter().collect::>()) + } else { + None + }; + + // + // First, get all of the SPs that we can see via Ignition + // + let all_sp_list = + mgs_client.ignition_list().await.context("listing ignition")?; + + let mut sp_list = all_sp_list + .iter() + .filter_map(|ignition| { + if matches!(ignition.details, SpIgnition::Yes { .. }) { + if ignition.id.type_ == SpType::Sled { + let matched = if args.sleds.len() > 0 { + args.sleds + .iter() + .find(|&&v| v == ignition.id.slot) + .is_some() + } else { + true + }; + + if matched != args.exclude { + return Some(ignition.id); + } + } + } + None + }) + .collect::>(); + + if args.switches { + sp_list.push(SpIdentifier { type_: SpType::Switch, slot: 0 }); + sp_list.push(SpIdentifier { type_: SpType::Switch, slot: 1 }); + } + + if args.psc { + sp_list.push(SpIdentifier { type_: SpType::Power, slot: 0 }); + } + + sp_list.sort(); + + let now = std::time::Instant::now(); + + let mut handles = vec![]; + for sp_id in sp_list { + let mgs_client = mgs_client.clone(); + let type_ = sp_id.type_; + let slot = sp_id.slot; + + let handle = + tokio::spawn(async move { sp_info(mgs_client, type_, slot).await }); + + handles.push((sp_id, handle)); + } + + let mut sensors_by_sensor = MultiMap::new(); + let mut sensors_by_sensor_and_sp = HashMap::new(); + let mut sensors_by_id = HashMap::new(); + let mut sensors_by_sp = MultiMap::new(); + let mut all_values = HashMap::new(); + let mut work_by_sp = HashMap::new(); + + let mut current = 0; + + for (sp_id, handle) in handles { + let mut sp_work = vec![]; + + match handle.await.unwrap() { + Ok(info) => { + for (device, sensors) in info.devices { + let mut device_work = vec![]; + + for (sensor, value) in sensors { + if let Some(ref by_kind) = by_kind { + if by_kind.get(&sensor.kind).is_none() { + continue; + } + } + + if let Some(ref by_name) = by_name { + if by_name.get(&sensor.name).is_none() { + continue; + } + } + + let id = SensorId(current); + current += 1; + + sensors_by_id.insert( + id, + (sp_id, sensor.clone(), device.clone()), + ); + + if value.is_none() && args.verbose { + eprintln!( + "mgs: error for {sp_id:?} on {sensor:?} ({device})" + ); + } + + sensors_by_sensor.insert(sensor.clone(), id); + + let by_sp = sensors_by_sensor_and_sp + .entry(sensor) + .or_insert_with(|| HashMap::new()); + by_sp.insert(sp_id, id); + sensors_by_sp.insert(sp_id, id); + all_values.insert(id, value); + + device_work.push(id); + } + + sp_work.push((device, device_work)); + } + + if args.verbose { + eprintln!( + "mgs: latencies for {sp_id:?}: {:.1?} {:.1?}", + info.timestamps[2].duration_since(info.timestamps[1]), + info.timestamps[1].duration_since(info.timestamps[0]) + ); + } + } + Err(err) => { + eprintln!("failed to read devices for {:?}: {:?}", sp_id, err); + } + } + + work_by_sp.insert(sp_id, sp_work); + } + + if args.verbose { + eprintln!("total discovery time {:?}", now.elapsed()); + } + + Ok(( + SensorMetadata { + sensors_by_sensor, + sensors_by_sensor_and_sp, + sensors_by_id, + sensors_by_sp, + work_by_sp, + }, + SensorValues(all_values), + )) +} + +async fn sp_read_sensors( + mgs_client: &gateway_client::Client, + id: &SpIdentifier, + metadata: std::sync::Arc<&SensorMetadata>, +) -> Result)>, anyhow::Error> { + let work = metadata.work_by_sp.get(id).unwrap(); + let mut rval = vec![]; + + for (component, ids) in work.iter() { + for (value, id) in mgs_client + .sp_component_get(id.type_, id.slot, component) + .await? + .iter() + .filter_map(|detail| match detail { + SpComponentDetails::Measurement { kind: _, name: _, value } => { + Some(Some(*value)) + } + SpComponentDetails::MeasurementError { error, .. } => { + match error { + MeasurementErrorCode::NoReading + | MeasurementErrorCode::NotPresent => None, + _ => Some(None), + } + } + _ => None, + }) + .zip(ids.iter()) + { + rval.push((*id, value)); + } + } + + Ok(rval) +} + +async fn sensor_data( + mgs_client: &gateway_client::Client, + metadata: std::sync::Arc<&'static SensorMetadata>, +) -> Result { + let mut all_values = HashMap::new(); + let mut handles = vec![]; + + for sp_id in metadata.sensors_by_sp.keys() { + let mgs_client = mgs_client.clone(); + let id = *sp_id; + let metadata = metadata.clone(); + + let handle = tokio::spawn(async move { + sp_read_sensors(&mgs_client, &id, metadata).await + }); + + handles.push(handle); + } + + for handle in handles { + let rval = handle.await.unwrap()?; + + for (id, value) in rval { + all_values.insert(id, value); + } + } + + Ok(SensorValues(all_values)) +} + +/// +/// Runs `omdb mgs sensors` +/// +pub(crate) async fn cmd_mgs_sensors( + mgs_client: &gateway_client::Client, + args: &SensorsArgs, +) -> Result<(), anyhow::Error> { + let (metadata, mut values) = sensor_metadata(mgs_client, args).await?; + + // + // A bit of shenangians to force metadata to be 'static -- which allows + // us to share it with tasks. + // + let metadata = Box::leak(Box::new(metadata)); + let metadata: &_ = metadata; + let metadata = std::sync::Arc::new(metadata); + + let mut sensors = metadata.sensors_by_sensor.keys().collect::>(); + sensors.sort(); + + let mut sps = metadata.sensors_by_sp.keys().collect::>(); + sps.sort(); + + let print_value = |v| { + if args.parseable { + print!(",{v}"); + } else { + print!(" {v:>8}"); + } + }; + + let print_header = || { + if !args.parseable { + print!("{:20} ", "NAME"); + } else { + print!("TIME,SENSOR"); + } + + for sp in &sps { + print_value(format!( + "{}-{}", + crate::mgs::sp_type_to_str(&sp.type_).to_uppercase(), + sp.slot + )); + } + + println!(); + }; + + let print_name = |sensor: &Sensor, now: Duration| { + if !args.parseable { + print!("{:20} ", sensor.name); + } else { + print!( + "{},{},{}", + now.as_secs(), + sensor.name, + sensor.to_kind_string() + ); + } + }; + + let mut wakeup = + tokio::time::Instant::now() + tokio::time::Duration::from_millis(1000); + + print_header(); + + loop { + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; + + for sensor in &sensors { + print_name(sensor, now); + + let by_sp = metadata.sensors_by_sensor_and_sp.get(sensor).unwrap(); + + for sp in &sps { + print_value(if let Some(id) = by_sp.get(sp) { + if let Some(value) = values.0.get(id) { + match value { + Some(value) => { + sensor.format(*value, args.parseable) + } + None => "X".to_string(), + } + } else { + "?".to_string() + } + } else { + "-".to_string() + }); + } + + println!(); + } + + if !args.sleep { + break; + } + + tokio::time::sleep_until(wakeup).await; + wakeup += tokio::time::Duration::from_millis(1000); + + values = sensor_data(mgs_client, metadata.clone()).await?; + + if !args.parseable { + print_header(); + } + } + + Ok(()) +} From ab2c3c7bd547812a0d5bc512aadd0ad5d83b9ee1 Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Mon, 1 Jan 2024 19:59:33 +0000 Subject: [PATCH 05/21] little refactor --- dev-tools/omdb/src/bin/omdb/mgs.rs | 16 ++++++++++++---- dev-tools/omdb/src/bin/omdb/mgs/sensors.rs | 19 ++++++++++++++++--- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/dev-tools/omdb/src/bin/omdb/mgs.rs b/dev-tools/omdb/src/bin/omdb/mgs.rs index c7305b90cb..78643d5220 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs.rs @@ -55,11 +55,11 @@ enum MgsCommands { struct InventoryArgs {} impl MgsArgs { - pub(crate) async fn run_cmd( + async fn mgs_client( &self, omdb: &Omdb, - log: &slog::Logger, - ) -> Result<(), anyhow::Error> { + log: &slog::Logger + ) -> Result { let mgs_url = match &self.mgs_url { Some(cli_or_env_url) => cli_or_env_url.clone(), None => { @@ -80,7 +80,15 @@ impl MgsArgs { } }; eprintln!("note: using MGS URL {}", &mgs_url); - let mgs_client = gateway_client::Client::new(&mgs_url, log.clone()); + Ok(gateway_client::Client::new(&mgs_url, log.clone())) + } + + pub(crate) async fn run_cmd( + &self, + omdb: &Omdb, + log: &slog::Logger, + ) -> Result<(), anyhow::Error> { + let mgs_client = self.mgs_client(omdb, log).await?; match &self.command { MgsCommands::Dashboard(dashboard_args) => { diff --git a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs index 64419bdff9..7b586763b0 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs @@ -129,6 +129,11 @@ impl Sensor { } } +enum SensorInput<'a> { + MgsClient(&'a gateway_client::Client), + File(String), +} + #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] struct SensorId(u32); @@ -200,10 +205,17 @@ async fn sp_info( Ok(SpInfo { devices, timestamps }) } -async fn sensor_metadata( - mgs_client: &gateway_client::Client, +async fn sensor_metadata ( + input: SensorInput<'_>, args: &SensorsArgs, ) -> Result<(SensorMetadata, SensorValues), anyhow::Error> { + let mgs_client = match input { + SensorInput::MgsClient(client) => client, + _ => { + bail!("not yet"); + } + }; + let by_kind = if let Some(types) = &args.types { let mut h = HashSet::new(); @@ -442,7 +454,8 @@ pub(crate) async fn cmd_mgs_sensors( mgs_client: &gateway_client::Client, args: &SensorsArgs, ) -> Result<(), anyhow::Error> { - let (metadata, mut values) = sensor_metadata(mgs_client, args).await?; + let input = SensorInput::MgsClient(mgs_client); + let (metadata, mut values) = sensor_metadata(input, args).await?; // // A bit of shenangians to force metadata to be 'static -- which allows From 918c99d213cf809a6408bcdfa5f81d0d19870b2f Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Mon, 1 Jan 2024 21:45:09 +0000 Subject: [PATCH 06/21] refactor --- Cargo.lock | 22 ++ Cargo.toml | 1 + dev-tools/omdb/Cargo.toml | 1 + dev-tools/omdb/src/bin/omdb/mgs.rs | 4 +- dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs | 6 +- dev-tools/omdb/src/bin/omdb/mgs/sensors.rs | 215 +++++++++++-------- 6 files changed, 152 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e4d1a936a1..0ca2a7b036 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1408,6 +1408,27 @@ dependencies = [ "memchr", ] +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + [[package]] name = "ctr" version = "0.9.2" @@ -4862,6 +4883,7 @@ dependencies = [ "chrono", "clap 4.4.3", "crucible-agent-client", + "csv", "diesel", "dropshot", "expectorate", diff --git a/Cargo.toml b/Cargo.toml index e1f8283d05..24649d7582 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -174,6 +174,7 @@ crossterm = { version = "0.27.0", features = ["event-stream"] } crucible-agent-client = { git = "https://github.com/oxidecomputer/crucible", rev = "fab27994d0bd12725c17d6b478a9bfc2673ad6f4" } crucible-pantry-client = { git = "https://github.com/oxidecomputer/crucible", rev = "fab27994d0bd12725c17d6b478a9bfc2673ad6f4" } crucible-smf = { git = "https://github.com/oxidecomputer/crucible", rev = "fab27994d0bd12725c17d6b478a9bfc2673ad6f4" } +csv = "1.3.0" curve25519-dalek = "4" datatest-stable = "0.2.3" display-error-chain = "0.2.0" diff --git a/dev-tools/omdb/Cargo.toml b/dev-tools/omdb/Cargo.toml index b137e049ac..b6ea611367 100644 --- a/dev-tools/omdb/Cargo.toml +++ b/dev-tools/omdb/Cargo.toml @@ -13,6 +13,7 @@ async-bb8-diesel.workspace = true chrono.workspace = true clap.workspace = true crucible-agent-client.workspace = true +csv.workspace = true diesel.workspace = true dropshot.workspace = true futures.workspace = true diff --git a/dev-tools/omdb/src/bin/omdb/mgs.rs b/dev-tools/omdb/src/bin/omdb/mgs.rs index 78643d5220..3a5cd4b2f5 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs.rs @@ -58,7 +58,7 @@ impl MgsArgs { async fn mgs_client( &self, omdb: &Omdb, - log: &slog::Logger + log: &slog::Logger, ) -> Result { let mgs_url = match &self.mgs_url { Some(cli_or_env_url) => cli_or_env_url.clone(), @@ -98,7 +98,7 @@ impl MgsArgs { cmd_mgs_inventory(&mgs_client, inventory_args).await } MgsCommands::Sensors(sensors_args) => { - sensors::cmd_mgs_sensors(&mgs_client, sensors_args).await + sensors::cmd_mgs_sensors(omdb, log, self, sensors_args).await } } } diff --git a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs index 31f8954014..5a7931cb78 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs @@ -7,11 +7,7 @@ use clap::Args; #[derive(Debug, Args)] -pub(crate) struct DashboardArgs { - /// simulate using specified file as input - #[clap(long, short)] - input: Option, -} +pub(crate) struct DashboardArgs {} /// /// Runs `omdb mgs dashboard` diff --git a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs index 7b586763b0..e733119bae 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs @@ -14,6 +14,7 @@ use gateway_client::types::SpIgnition; use gateway_client::types::SpType; use multimap::MultiMap; use std::collections::{HashMap, HashSet}; +use std::fs::File; use std::time::{Duration, SystemTime}; #[derive(Debug, Args)] @@ -63,6 +64,10 @@ pub(crate) struct SensorsArgs { use_value_delimiter = true )] named: Option>, + + /// simulate using specified file as input + #[clap(long, short)] + input: Option, } #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] @@ -129,9 +134,9 @@ impl Sensor { } } -enum SensorInput<'a> { - MgsClient(&'a gateway_client::Client), - File(String), +enum SensorInput { + MgsClient(gateway_client::Client), + CsvReader(csv::Reader), } #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] @@ -205,37 +210,11 @@ async fn sp_info( Ok(SpInfo { devices, timestamps }) } -async fn sensor_metadata ( - input: SensorInput<'_>, +async fn sp_info_mgs( + mgs_client: &gateway_client::Client, args: &SensorsArgs, -) -> Result<(SensorMetadata, SensorValues), anyhow::Error> { - let mgs_client = match input { - SensorInput::MgsClient(client) => client, - _ => { - bail!("not yet"); - } - }; - - let by_kind = if let Some(types) = &args.types { - let mut h = HashSet::new(); - - for t in types { - h.insert(match Sensor::from_string("", &t) { - None => bail!("invalid sensor kind {t}"), - Some(s) => s.kind, - }); - } - - Some(h) - } else { - None - }; - - let by_name = if let Some(named) = &args.named { - Some(named.into_iter().collect::>()) - } else { - None - }; +) -> Result, anyhow::Error> { + let mut rval = vec![]; // // First, get all of the SPs that we can see via Ignition @@ -291,6 +270,68 @@ async fn sensor_metadata ( handles.push((sp_id, handle)); } + for (sp_id, handle) in handles { + match handle.await.unwrap() { + Ok(info) => { + if args.verbose { + eprintln!( + "mgs: latencies for {sp_id:?}: {:.1?} {:.1?}", + info.timestamps[2].duration_since(info.timestamps[1]), + info.timestamps[1].duration_since(info.timestamps[0]) + ); + } + + rval.push((sp_id, info)); + } + + Err(err) => { + eprintln!("failed to read devices for {:?}: {:?}", sp_id, err); + } + } + } + + if args.verbose { + eprintln!("total discovery time {:?}", now.elapsed()); + } + + Ok(rval) +} + +async fn sensor_metadata( + input: &mut SensorInput, + args: &SensorsArgs, +) -> Result<(SensorMetadata, SensorValues), anyhow::Error> { + let by_kind = if let Some(types) = &args.types { + let mut h = HashSet::new(); + + for t in types { + h.insert(match Sensor::from_string("", &t) { + None => bail!("invalid sensor kind {t}"), + Some(s) => s.kind, + }); + } + + Some(h) + } else { + None + }; + + let by_name = if let Some(named) = &args.named { + Some(named.into_iter().collect::>()) + } else { + None + }; + + let info = match input { + SensorInput::MgsClient(ref mgs_client) => { + sp_info_mgs(mgs_client, args).await? + } + // SensorInput::CsvReader(XXX) => sp_info_csv(mgs_client), + _ => { + bail!("not yet"); + } + }; + let mut sensors_by_sensor = MultiMap::new(); let mut sensors_by_sensor_and_sp = HashMap::new(); let mut sensors_by_id = HashMap::new(); @@ -300,76 +341,55 @@ async fn sensor_metadata ( let mut current = 0; - for (sp_id, handle) in handles { + for (sp_id, info) in info { let mut sp_work = vec![]; - match handle.await.unwrap() { - Ok(info) => { - for (device, sensors) in info.devices { - let mut device_work = vec![]; - - for (sensor, value) in sensors { - if let Some(ref by_kind) = by_kind { - if by_kind.get(&sensor.kind).is_none() { - continue; - } - } - - if let Some(ref by_name) = by_name { - if by_name.get(&sensor.name).is_none() { - continue; - } - } - - let id = SensorId(current); - current += 1; + for (device, sensors) in info.devices { + let mut device_work = vec![]; - sensors_by_id.insert( - id, - (sp_id, sensor.clone(), device.clone()), - ); - - if value.is_none() && args.verbose { - eprintln!( - "mgs: error for {sp_id:?} on {sensor:?} ({device})" - ); - } - - sensors_by_sensor.insert(sensor.clone(), id); - - let by_sp = sensors_by_sensor_and_sp - .entry(sensor) - .or_insert_with(|| HashMap::new()); - by_sp.insert(sp_id, id); - sensors_by_sp.insert(sp_id, id); - all_values.insert(id, value); - - device_work.push(id); + for (sensor, value) in sensors { + if let Some(ref by_kind) = by_kind { + if by_kind.get(&sensor.kind).is_none() { + continue; } + } - sp_work.push((device, device_work)); + if let Some(ref by_name) = by_name { + if by_name.get(&sensor.name).is_none() { + continue; + } } - if args.verbose { + let id = SensorId(current); + current += 1; + + sensors_by_id + .insert(id, (sp_id, sensor.clone(), device.clone())); + + if value.is_none() && args.verbose { eprintln!( - "mgs: latencies for {sp_id:?}: {:.1?} {:.1?}", - info.timestamps[2].duration_since(info.timestamps[1]), - info.timestamps[1].duration_since(info.timestamps[0]) + "mgs: error for {sp_id:?} on {sensor:?} ({device})" ); } + + sensors_by_sensor.insert(sensor.clone(), id); + + let by_sp = sensors_by_sensor_and_sp + .entry(sensor) + .or_insert_with(|| HashMap::new()); + by_sp.insert(sp_id, id); + sensors_by_sp.insert(sp_id, id); + all_values.insert(id, value); + + device_work.push(id); } - Err(err) => { - eprintln!("failed to read devices for {:?}: {:?}", sp_id, err); - } + + sp_work.push((device, device_work)); } work_by_sp.insert(sp_id, sp_work); } - if args.verbose { - eprintln!("total discovery time {:?}", now.elapsed()); - } - Ok(( SensorMetadata { sensors_by_sensor, @@ -451,11 +471,19 @@ async fn sensor_data( /// Runs `omdb mgs sensors` /// pub(crate) async fn cmd_mgs_sensors( - mgs_client: &gateway_client::Client, + omdb: &crate::Omdb, + log: &slog::Logger, + mgs_args: &crate::mgs::MgsArgs, args: &SensorsArgs, ) -> Result<(), anyhow::Error> { - let input = SensorInput::MgsClient(mgs_client); - let (metadata, mut values) = sensor_metadata(input, args).await?; + let mut input = if let Some(ref input) = args.input { + let file = File::open(input)?; + SensorInput::CsvReader(csv::Reader::from_reader(file)) + } else { + SensorInput::MgsClient(mgs_args.mgs_client(omdb, log).await?) + }; + + let (metadata, mut values) = sensor_metadata(&mut input, args).await?; // // A bit of shenangians to force metadata to be 'static -- which allows @@ -465,6 +493,13 @@ pub(crate) async fn cmd_mgs_sensors( let metadata: &_ = metadata; let metadata = std::sync::Arc::new(metadata); + let mgs_client = match input { + SensorInput::MgsClient(ref client) => client, + _ => { + bail!("not yet"); + } + }; + let mut sensors = metadata.sensors_by_sensor.keys().collect::>(); sensors.sort(); From 7ecb50bca12bba6b117a12338821599a7340d2c3 Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Tue, 2 Jan 2024 05:14:47 +0000 Subject: [PATCH 07/21] read from CSV --- dev-tools/omdb/src/bin/omdb/mgs.rs | 7 +- dev-tools/omdb/src/bin/omdb/mgs/sensors.rs | 196 ++++++++++++++++++--- 2 files changed, 170 insertions(+), 33 deletions(-) diff --git a/dev-tools/omdb/src/bin/omdb/mgs.rs b/dev-tools/omdb/src/bin/omdb/mgs.rs index 3a5cd4b2f5..c5ebbfd134 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs.rs @@ -5,7 +5,7 @@ //! Prototype code for collecting information from systems in the rack use crate::Omdb; -use anyhow::Context; +use anyhow::{bail, Context}; use clap::Args; use clap::Subcommand; use futures::StreamExt; @@ -88,13 +88,12 @@ impl MgsArgs { omdb: &Omdb, log: &slog::Logger, ) -> Result<(), anyhow::Error> { - let mgs_client = self.mgs_client(omdb, log).await?; - match &self.command { MgsCommands::Dashboard(dashboard_args) => { - dashboard::cmd_mgs_dashboard(&mgs_client, dashboard_args).await + bail!("no"); } MgsCommands::Inventory(inventory_args) => { + let mgs_client = self.mgs_client(omdb, log).await?; cmd_mgs_inventory(&mgs_client, inventory_args).await } MgsCommands::Sensors(sensors_args) => { diff --git a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs index e733119bae..200bca2ac6 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs @@ -70,6 +70,24 @@ pub(crate) struct SensorsArgs { input: Option, } +impl SensorsArgs { + fn matches_sp(&self, sp: &SpIdentifier) -> bool { + match sp.type_ { + SpType::Sled => { + let matched = if self.sleds.len() > 0 { + self.sleds.iter().find(|&&v| v == sp.slot).is_some() + } else { + true + }; + + matched != self.exclude + } + SpType::Switch => self.switches, + SpType::Power => self.psc, + } + } +} + #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] struct Sensor { name: String, @@ -136,7 +154,7 @@ impl Sensor { enum SensorInput { MgsClient(gateway_client::Client), - CsvReader(csv::Reader), + CsvReader(csv::Reader, csv::Position), } #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] @@ -146,15 +164,32 @@ struct SensorId(u32); struct SensorMetadata { sensors_by_sensor: MultiMap, sensors_by_sensor_and_sp: HashMap>, - sensors_by_id: HashMap, + sensors_by_id: HashMap, sensors_by_sp: MultiMap, - work_by_sp: HashMap)>>, + work_by_sp: HashMap)>>, } struct SensorValues(HashMap>); +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +enum DeviceIdentifier { + Field(usize), + Device(String), +} + +impl DeviceIdentifier { + fn device(&self) -> &String { + match self { + Self::Device(ref device) => device, + _ => { + panic!("tried to treat non-device as device") + } + } + } +} + struct SpInfo { - devices: MultiMap)>, + devices: MultiMap)>, timestamps: Vec, } @@ -201,7 +236,7 @@ async fn sp_info( _ => None, }) { - devices.insert(c.component.clone(), s); + devices.insert(DeviceIdentifier::Device(c.component.clone()), s); } } @@ -227,16 +262,7 @@ async fn sp_info_mgs( .filter_map(|ignition| { if matches!(ignition.details, SpIgnition::Yes { .. }) { if ignition.id.type_ == SpType::Sled { - let matched = if args.sleds.len() > 0 { - args.sleds - .iter() - .find(|&&v| v == ignition.id.slot) - .is_some() - } else { - true - }; - - if matched != args.exclude { + if args.matches_sp(&ignition.id) { return Some(ignition.id); } } @@ -297,7 +323,117 @@ async fn sp_info_mgs( Ok(rval) } -async fn sensor_metadata( +fn sp_info_csv( + reader: &mut csv::Reader, + position: &mut csv::Position, + args: &SensorsArgs, +) -> Result, anyhow::Error> { + let mut sps = vec![]; + let headers = reader.headers()?; + + let expected = ["TIME", "SENSOR", "KIND"]; + let len = expected.len(); + let hlen = headers.len(); + + if hlen < len { + bail!("expected as least {len} fields (found {headers:?})"); + } + + for ndx in 0..len { + if &headers[ndx] != expected[ndx] { + bail!( + "malformed headers: expected {}, found {} ({headers:?})", + &expected[ndx], + &headers[ndx] + ); + } + } + + for ndx in len..hlen { + let field = &headers[ndx]; + let parts: Vec<&str> = field.splitn(2, '-').collect(); + + if parts.len() != 2 { + bail!("malformed field \"{field}\""); + } + + let type_ = match parts[0] { + "SLED" => SpType::Sled, + "SWITCH" => SpType::Switch, + "POWER" => SpType::Power, + _ => { + bail!("unknown type {}", parts[0]); + } + }; + + let slot = parts[1].parse::().or_else(|_| { + bail!("invalid slot in \"{field}\""); + })?; + + let sp = SpIdentifier { type_, slot }; + + if args.matches_sp(&sp) { + sps.push(Some(sp)); + } else { + sps.push(None); + } + } + + let mut iter = reader.records(); + let mut sensors = HashSet::new(); + let mut by_sp = MultiMap::new(); + + loop { + *position = iter.reader().position().clone(); + + if let Some(record) = iter.next() { + let record = record?; + + if record.len() != hlen { + bail!("bad record length at line {}", position.line()); + } + + if let Some(sensor) = Sensor::from_string(&record[1], &record[2]) { + if sensors.get(&sensor).is_some() { + break; + } + + sensors.insert(sensor.clone()); + + for (ndx, sp) in sps.iter().enumerate() { + if let Some(sp) = sp { + let value = match record[ndx + 3].parse::() { + Ok(value) => Some(value), + _ => None, + }; + + by_sp.insert(sp, (sensor.clone(), value)); + } + } + } + } else { + break; + } + } + + let mut rval = vec![]; + + for (field, sp) in sps.iter().enumerate() { + let mut devices = MultiMap::new(); + + if let Some(sp) = sp { + if let Some(v) = by_sp.remove(sp) { + devices.insert_many(DeviceIdentifier::Field(field), v); + } + + rval.push((*sp, SpInfo { devices, timestamps: vec![] })); + } + } + + Ok(rval) +} + +async fn sensor_metadata( input: &mut SensorInput, args: &SensorsArgs, ) -> Result<(SensorMetadata, SensorValues), anyhow::Error> { @@ -326,9 +462,8 @@ async fn sensor_metadata( SensorInput::MgsClient(ref mgs_client) => { sp_info_mgs(mgs_client, args).await? } - // SensorInput::CsvReader(XXX) => sp_info_csv(mgs_client), - _ => { - bail!("not yet"); + SensorInput::CsvReader(reader, position) => { + sp_info_csv(reader, position, args)? } }; @@ -368,7 +503,7 @@ async fn sensor_metadata( if value.is_none() && args.verbose { eprintln!( - "mgs: error for {sp_id:?} on {sensor:?} ({device})" + "mgs: error for {sp_id:?} on {sensor:?} ({device:?})" ); } @@ -412,7 +547,7 @@ async fn sp_read_sensors( for (component, ids) in work.iter() { for (value, id) in mgs_client - .sp_component_get(id.type_, id.slot, component) + .sp_component_get(id.type_, id.slot, component.device()) .await? .iter() .filter_map(|detail| match detail { @@ -478,7 +613,10 @@ pub(crate) async fn cmd_mgs_sensors( ) -> Result<(), anyhow::Error> { let mut input = if let Some(ref input) = args.input { let file = File::open(input)?; - SensorInput::CsvReader(csv::Reader::from_reader(file)) + SensorInput::CsvReader( + csv::Reader::from_reader(file), + csv::Position::new(), + ) } else { SensorInput::MgsClient(mgs_args.mgs_client(omdb, log).await?) }; @@ -493,13 +631,6 @@ pub(crate) async fn cmd_mgs_sensors( let metadata: &_ = metadata; let metadata = std::sync::Arc::new(metadata); - let mgs_client = match input { - SensorInput::MgsClient(ref client) => client, - _ => { - bail!("not yet"); - } - }; - let mut sensors = metadata.sensors_by_sensor.keys().collect::>(); sensors.sort(); @@ -585,6 +716,13 @@ pub(crate) async fn cmd_mgs_sensors( tokio::time::sleep_until(wakeup).await; wakeup += tokio::time::Duration::from_millis(1000); + let mgs_client = match input { + SensorInput::MgsClient(ref client) => client, + _ => { + bail!("nope"); + } + }; + values = sensor_data(mgs_client, metadata.clone()).await?; if !args.parseable { From dfed3b16efd22f2a60d272b63c4be3b439c4e97d Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Tue, 2 Jan 2024 06:52:03 +0000 Subject: [PATCH 08/21] CSV reading --- dev-tools/omdb/src/bin/omdb/mgs/sensors.rs | 92 +++++++++++++++++++--- 1 file changed, 79 insertions(+), 13 deletions(-) diff --git a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs index 200bca2ac6..08cd76fab3 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs @@ -181,9 +181,14 @@ impl DeviceIdentifier { fn device(&self) -> &String { match self { Self::Device(ref device) => device, - _ => { - panic!("tried to treat non-device as device") - } + _ => panic!(), + } + } + + fn field(&self) -> usize { + match self { + Self::Field(field) => *field, + _ => panic!(), } } } @@ -572,7 +577,7 @@ async fn sp_read_sensors( Ok(rval) } -async fn sensor_data( +async fn sp_data_mgs( mgs_client: &gateway_client::Client, metadata: std::sync::Arc<&'static SensorMetadata>, ) -> Result { @@ -602,6 +607,75 @@ async fn sensor_data( Ok(SensorValues(all_values)) } +fn sp_data_csv( + reader: &mut csv::Reader, + position: &mut csv::Position, + metadata: std::sync::Arc<&'static SensorMetadata>, +) -> Result { + let headers = reader.headers()?; + let hlen = headers.len(); + let mut all_values = HashMap::new(); + + reader.seek(position.clone())?; + let mut iter = reader.records(); + + let mut time: Option = None; + + loop { + *position = iter.reader().position().clone(); + + if let Some(record) = iter.next() { + let record = record?; + + if record.len() != hlen { + bail!("bad record length at line {}", position.line()); + } + + if let Some(ref time) = time { + if &record[0] != time { + break; + } + } else { + time = Some(record[0].to_string()); + } + + if let Some(sensor) = Sensor::from_string(&record[1], &record[2]) { + if let Some(ids) = metadata.sensors_by_sensor.get_vec(&sensor) { + for id in ids { + let (_, _, d) = metadata.sensors_by_id.get(id).unwrap(); + let field = d.field() + 3; + + let value = match record[field].parse::() { + Ok(value) => Some(value), + _ => None, + }; + + all_values.insert(*id, value); + } + } + } else { + bail!("bad sensor at line {}", position.line()); + } + } + } + + Ok(SensorValues(all_values)) +} + +async fn sensor_data( + input: &mut SensorInput, + metadata: std::sync::Arc<&'static SensorMetadata>, +) -> Result { + match input { + SensorInput::MgsClient(ref mgs_client) => { + sp_data_mgs(mgs_client, metadata).await + } + SensorInput::CsvReader(reader, position) => { + sp_data_csv(reader, position, metadata) + } + } +} + /// /// Runs `omdb mgs sensors` /// @@ -715,15 +789,7 @@ pub(crate) async fn cmd_mgs_sensors( tokio::time::sleep_until(wakeup).await; wakeup += tokio::time::Duration::from_millis(1000); - - let mgs_client = match input { - SensorInput::MgsClient(ref client) => client, - _ => { - bail!("nope"); - } - }; - - values = sensor_data(mgs_client, metadata.clone()).await?; + values = sensor_data(&mut input, metadata.clone()).await?; if !args.parseable { print_header(); From 902b5ecf61e88e3141797d0418b52fe41a82000b Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Tue, 2 Jan 2024 08:57:36 +0000 Subject: [PATCH 09/21] fix time --- dev-tools/omdb/src/bin/omdb/mgs/sensors.rs | 101 +++++++++++++-------- 1 file changed, 62 insertions(+), 39 deletions(-) diff --git a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs index 08cd76fab3..9f5e398f83 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs @@ -169,7 +169,10 @@ struct SensorMetadata { work_by_sp: HashMap)>>, } -struct SensorValues(HashMap>); +struct SensorValues { + values: HashMap>, + time: u64, +} #[derive(Clone, Debug, Hash, PartialEq, Eq)] enum DeviceIdentifier { @@ -253,7 +256,7 @@ async fn sp_info( async fn sp_info_mgs( mgs_client: &gateway_client::Client, args: &SensorsArgs, -) -> Result, anyhow::Error> { +) -> Result<(Vec<(SpIdentifier, SpInfo)>, u64), anyhow::Error> { let mut rval = vec![]; // @@ -325,14 +328,17 @@ async fn sp_info_mgs( eprintln!("total discovery time {:?}", now.elapsed()); } - Ok(rval) + Ok(( + rval, + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?.as_secs(), + )) } fn sp_info_csv( reader: &mut csv::Reader, position: &mut csv::Position, args: &SensorsArgs, -) -> Result, anyhow::Error> { +) -> Result<(Vec<(SpIdentifier, SpInfo)>, u64), anyhow::Error> { let mut sps = vec![]; let headers = reader.headers()?; @@ -387,6 +393,7 @@ fn sp_info_csv( let mut iter = reader.records(); let mut sensors = HashSet::new(); let mut by_sp = MultiMap::new(); + let mut time = None; loop { *position = iter.reader().position().clone(); @@ -398,6 +405,12 @@ fn sp_info_csv( bail!("bad record length at line {}", position.line()); } + if time.is_none() { + time = Some(record[0].parse::().or_else(|_| { + bail!("bad time at line {}", position.line()); + })?); + } + if let Some(sensor) = Sensor::from_string(&record[1], &record[2]) { if sensors.get(&sensor).is_some() { break; @@ -407,7 +420,7 @@ fn sp_info_csv( for (ndx, sp) in sps.iter().enumerate() { if let Some(sp) = sp { - let value = match record[ndx + 3].parse::() { + let value = match record[ndx + len].parse::() { Ok(value) => Some(value), _ => None, }; @@ -428,14 +441,14 @@ fn sp_info_csv( if let Some(sp) = sp { if let Some(v) = by_sp.remove(sp) { - devices.insert_many(DeviceIdentifier::Field(field), v); + devices.insert_many(DeviceIdentifier::Field(field + len), v); } rval.push((*sp, SpInfo { devices, timestamps: vec![] })); } } - Ok(rval) + Ok((rval, time.unwrap())) } async fn sensor_metadata( @@ -463,7 +476,7 @@ async fn sensor_metadata( None }; - let info = match input { + let (info, time) = match input { SensorInput::MgsClient(ref mgs_client) => { sp_info_mgs(mgs_client, args).await? } @@ -476,7 +489,7 @@ async fn sensor_metadata( let mut sensors_by_sensor_and_sp = HashMap::new(); let mut sensors_by_id = HashMap::new(); let mut sensors_by_sp = MultiMap::new(); - let mut all_values = HashMap::new(); + let mut values = HashMap::new(); let mut work_by_sp = HashMap::new(); let mut current = 0; @@ -519,7 +532,7 @@ async fn sensor_metadata( .or_insert_with(|| HashMap::new()); by_sp.insert(sp_id, id); sensors_by_sp.insert(sp_id, id); - all_values.insert(id, value); + values.insert(id, value); device_work.push(id); } @@ -538,7 +551,7 @@ async fn sensor_metadata( sensors_by_sp, work_by_sp, }, - SensorValues(all_values), + SensorValues { values, time }, )) } @@ -581,7 +594,7 @@ async fn sp_data_mgs( mgs_client: &gateway_client::Client, metadata: std::sync::Arc<&'static SensorMetadata>, ) -> Result { - let mut all_values = HashMap::new(); + let mut values = HashMap::new(); let mut handles = vec![]; for sp_id in metadata.sensors_by_sp.keys() { @@ -600,11 +613,16 @@ async fn sp_data_mgs( let rval = handle.await.unwrap()?; for (id, value) in rval { - all_values.insert(id, value); + values.insert(id, value); } } - Ok(SensorValues(all_values)) + Ok(SensorValues { + values, + time: SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH)? + .as_secs(), + }) } fn sp_data_csv( @@ -614,12 +632,12 @@ fn sp_data_csv( ) -> Result { let headers = reader.headers()?; let hlen = headers.len(); - let mut all_values = HashMap::new(); + let mut values = HashMap::new(); reader.seek(position.clone())?; let mut iter = reader.records(); - let mut time: Option = None; + let mut time = None; loop { *position = iter.reader().position().clone(); @@ -631,35 +649,40 @@ fn sp_data_csv( bail!("bad record length at line {}", position.line()); } - if let Some(ref time) = time { - if &record[0] != time { + let now = record[0].parse::().or_else(|_| { + bail!("bad time at line {}", position.line()); + })?; + + if let Some(time) = time { + if now != time { break; } } else { - time = Some(record[0].to_string()); + time = Some(now); } if let Some(sensor) = Sensor::from_string(&record[1], &record[2]) { if let Some(ids) = metadata.sensors_by_sensor.get_vec(&sensor) { for id in ids { let (_, _, d) = metadata.sensors_by_id.get(id).unwrap(); - let field = d.field() + 3; - - let value = match record[field].parse::() { + let value = match record[d.field()].parse::() { Ok(value) => Some(value), _ => None, }; - all_values.insert(*id, value); + values.insert(*id, value); } } } else { bail!("bad sensor at line {}", position.line()); } + } else { + time = Some(0); + break; } } - Ok(SensorValues(all_values)) + Ok(SensorValues { values, time: time.unwrap() }) } async fn sensor_data( @@ -723,7 +746,7 @@ pub(crate) async fn cmd_mgs_sensors( if !args.parseable { print!("{:20} ", "NAME"); } else { - print!("TIME,SENSOR"); + print!("TIME,SENSOR,KIND"); } for sp in &sps { @@ -737,16 +760,11 @@ pub(crate) async fn cmd_mgs_sensors( println!(); }; - let print_name = |sensor: &Sensor, now: Duration| { + let print_name = |sensor: &Sensor, now: u64| { if !args.parseable { print!("{:20} ", sensor.name); } else { - print!( - "{},{},{}", - now.as_secs(), - sensor.name, - sensor.to_kind_string() - ); + print!("{now},{},{}", sensor.name, sensor.to_kind_string()); } }; @@ -756,16 +774,14 @@ pub(crate) async fn cmd_mgs_sensors( print_header(); loop { - let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; - for sensor in &sensors { - print_name(sensor, now); + print_name(sensor, values.time); let by_sp = metadata.sensors_by_sensor_and_sp.get(sensor).unwrap(); for sp in &sps { print_value(if let Some(id) = by_sp.get(sp) { - if let Some(value) = values.0.get(id) { + if let Some(value) = values.values.get(id) { match value { Some(value) => { sensor.format(*value, args.parseable) @@ -784,13 +800,20 @@ pub(crate) async fn cmd_mgs_sensors( } if !args.sleep { - break; + if args.input.is_none() { + break; + } + } else { + tokio::time::sleep_until(wakeup).await; + wakeup += tokio::time::Duration::from_millis(1000); } - tokio::time::sleep_until(wakeup).await; - wakeup += tokio::time::Duration::from_millis(1000); values = sensor_data(&mut input, metadata.clone()).await?; + if args.input.is_some() && values.time == 0 { + break; + } + if !args.parseable { print_header(); } From fadd0666f7fe8989529f995ac711f22103297880 Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Wed, 3 Jan 2024 04:28:57 +0000 Subject: [PATCH 10/21] placeholder dashboard --- Cargo.lock | 2 + dev-tools/omdb/Cargo.toml | 2 + dev-tools/omdb/src/bin/omdb/mgs.rs | 12 +- dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs | 696 ++++++++++++++++++- 4 files changed, 702 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ca2a7b036..ddadee0135 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4882,6 +4882,7 @@ dependencies = [ "async-bb8-diesel", "chrono", "clap 4.4.3", + "crossterm 0.27.0", "crucible-agent-client", "csv", "diesel", @@ -4908,6 +4909,7 @@ dependencies = [ "omicron-workspace-hack", "oximeter-client", "pq-sys", + "ratatui", "regex", "serde", "serde_json", diff --git a/dev-tools/omdb/Cargo.toml b/dev-tools/omdb/Cargo.toml index b6ea611367..ed47732384 100644 --- a/dev-tools/omdb/Cargo.toml +++ b/dev-tools/omdb/Cargo.toml @@ -12,6 +12,7 @@ anyhow.workspace = true async-bb8-diesel.workspace = true chrono.workspace = true clap.workspace = true +crossterm.workspace = true crucible-agent-client.workspace = true csv.workspace = true diesel.workspace = true @@ -30,6 +31,7 @@ omicron-common.workspace = true oximeter-client.workspace = true # See omicron-rpaths for more about the "pq-sys" dependency. pq-sys = "*" +ratatui.workspace = true serde.workspace = true serde_json.workspace = true sled-agent-client.workspace = true diff --git a/dev-tools/omdb/src/bin/omdb/mgs.rs b/dev-tools/omdb/src/bin/omdb/mgs.rs index c5ebbfd134..bb34c5ab5b 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs.rs @@ -89,15 +89,15 @@ impl MgsArgs { log: &slog::Logger, ) -> Result<(), anyhow::Error> { match &self.command { - MgsCommands::Dashboard(dashboard_args) => { - bail!("no"); + MgsCommands::Dashboard(args) => { + dashboard::cmd_mgs_dashboard(omdb, log, self, args).await } - MgsCommands::Inventory(inventory_args) => { + MgsCommands::Inventory(args) => { let mgs_client = self.mgs_client(omdb, log).await?; - cmd_mgs_inventory(&mgs_client, inventory_args).await + cmd_mgs_inventory(&mgs_client, args).await } - MgsCommands::Sensors(sensors_args) => { - sensors::cmd_mgs_sensors(omdb, log, self, sensors_args).await + MgsCommands::Sensors(args) => { + sensors::cmd_mgs_sensors(omdb, log, self, args).await } } } diff --git a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs index 5a7931cb78..5e4b49e6d1 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs @@ -4,17 +4,705 @@ //! Code for the MGS dashboard subcommand +use anyhow::{anyhow, bail, Result}; +use clap::{CommandFactory, Parser}; +use crossterm::{ + event::{ + self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, + KeyModifiers, + }, + execute, + terminal::{ + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, + LeaveAlternateScreen, + }, +}; +use ratatui::{ + backend::{Backend, CrosstermBackend}, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + symbols, + text::{Line, Span}, + widgets::{ + Axis, Block, Borders, Chart, Dataset, List, ListItem, ListState, + Paragraph, + }, + Frame, Terminal, +}; + use clap::Args; +use std::io; +use std::time::{Duration, Instant}; #[derive(Debug, Args)] -pub(crate) struct DashboardArgs {} +pub(crate) struct DashboardArgs { + #[clap(flatten)] + sensors_args: crate::mgs::sensors::SensorsArgs, +} + +struct StatefulList { + state: ListState, + n: usize, +} + +impl StatefulList { + fn next(&mut self) { + self.state.select(match self.state.selected() { + Some(ndx) => Some((ndx + 1) % self.n), + None => Some(0), + }); + } + + fn previous(&mut self) { + self.state.select(match self.state.selected() { + Some(ndx) if ndx == 0 => Some(self.n - 1), + Some(ndx) => Some(ndx - 1), + None => Some(0), + }); + } + + fn unselect(&mut self) { + self.state.select(None); + } +} + +struct Series { + name: String, + color: Color, + data: Vec<(f64, f64)>, + raw: Vec>, +} + +trait Attributes { + fn label(&self) -> String; + fn legend_label(&self) -> String; + fn x_axis_label(&self) -> String { + "Time".to_string() + } + fn y_axis_label(&self) -> String; + fn axis_value(&self, val: f64) -> String; + fn legend_value(&self, val: f64) -> String; + + fn increase(&mut self, _ndx: usize) -> Option { + None + } + + fn decrease(&mut self, _ndx: usize) -> Option { + None + } + + fn clear(&mut self) {} +} + +struct TempGraph; + +impl Attributes for TempGraph { + fn label(&self) -> String { + "Temperature".to_string() + } + fn legend_label(&self) -> String { + "Sensors".to_string() + } + + fn y_axis_label(&self) -> String { + "Degrees Celsius".to_string() + } + + fn axis_value(&self, val: f64) -> String { + format!("{:2.0}°", val) + } + + fn legend_value(&self, val: f64) -> String { + format!("{:4.2}°", val) + } +} + +struct FanGraph(Vec); + +impl FanGraph { + fn new(len: usize) -> Self { + let v = vec![0; len]; + FanGraph(v) + } +} + +impl Attributes for FanGraph { + fn label(&self) -> String { + "Fan speed".to_string() + } + fn legend_label(&self) -> String { + "Fans".to_string() + } + + fn y_axis_label(&self) -> String { + "RPM".to_string() + } + + fn axis_value(&self, val: f64) -> String { + format!("{:3.1}K", val / 1000.0) + } + + fn legend_value(&self, val: f64) -> String { + format!("{:.0}", val) + } + + fn increase(&mut self, ndx: usize) -> Option { + let current = self.0[ndx]; + let nval = current + 20; + + self.0[ndx] = if nval <= 100 { nval } else { 100 }; + Some(self.0[ndx]) + } + + fn decrease(&mut self, ndx: usize) -> Option { + let current = self.0[ndx]; + let nval = current as i8 - 20; + + self.0[ndx] = if nval >= 0 { nval as u8 } else { 0 }; + Some(self.0[ndx]) + } + + fn clear(&mut self) { + for val in self.0.iter_mut() { + *val = 0; + } + } +} + +struct CurrentGraph; + +impl Attributes for CurrentGraph { + fn label(&self) -> String { + "Output current".to_string() + } + fn legend_label(&self) -> String { + "Regulators".to_string() + } + + fn y_axis_label(&self) -> String { + "Amperes".to_string() + } + + fn axis_value(&self, val: f64) -> String { + format!("{:2.2}A", val) + } + + fn legend_value(&self, val: f64) -> String { + format!("{:3.2}A", val) + } +} + +struct Graph { + series: Vec, + legend: StatefulList, + time: usize, + width: usize, + interpolate: usize, + bounds: [f64; 2], + attributes: Box, +} + +impl Graph { + fn new(all: &[String], attr: Box) -> Result { + let mut series = vec![]; + + let colors = [ + Color::Yellow, + Color::Green, + Color::Magenta, + Color::White, + Color::Red, + Color::LightRed, + Color::Blue, + Color::LightMagenta, + Color::LightYellow, + Color::LightCyan, + Color::LightGreen, + Color::LightBlue, + Color::LightRed, + ]; + + for (ndx, s) in all.iter().enumerate() { + series.push(Series { + name: s.to_string(), + color: colors[ndx % colors.len()], + data: Vec::new(), + raw: Vec::new(), + }) + } + + Ok(Graph { + series, + legend: StatefulList { state: ListState::default(), n: all.len() }, + time: 0, + width: 600, + interpolate: 0, + bounds: [20.0, 120.0], + attributes: attr, + }) + } + + fn data(&mut self, data: &[Option]) { + for (ndx, s) in self.series.iter_mut().enumerate() { + s.raw.push(data[ndx]); + } + + self.time += 1; + } + + fn update_data(&mut self) { + for s in &mut self.series { + s.data = Vec::new(); + } + + for i in 0..self.width { + if self.time < self.width - i { + continue; + } + + let offs = self.time - (self.width - i); + + for (_ndx, s) in &mut self.series.iter_mut().enumerate() { + if let Some(datum) = s.raw[offs] { + let point = (i as f64, datum as f64); + + if self.interpolate != 0 { + if let Some(last) = s.data.last() { + let x_delta = point.0 - last.0; + let slope = (point.1 - last.1) / x_delta; + let x_inc = x_delta / self.interpolate as f64; + + for x in 0..self.interpolate { + s.data.push(( + point.0 + x as f64 * x_inc, + point.1 + (slope * x_inc), + )); + } + } + } + + s.data.push((i as f64, datum as f64)); + } + } + } + + self.update_bounds(); + } + + fn update_bounds(&mut self) { + let selected = self.legend.state.selected(); + let mut min = None; + let mut max = None; + + for (ndx, s) in self.series.iter().enumerate() { + if let Some(selected) = selected { + if ndx != selected { + continue; + } + } + + for (_, datum) in &s.data { + min = match min { + Some(min) if datum < min => Some(datum), + None => Some(datum), + _ => min, + }; + + max = match max { + Some(max) if datum > max => Some(datum), + None => Some(datum), + _ => max, + }; + } + } + + if let Some(min) = min { + self.bounds[0] = ((min * 0.85) / 2.0) * 2.0; + } + + if self.bounds[0] < 0.0 { + self.bounds[0] = 0.0; + } + + if let Some(max) = max { + self.bounds[1] = ((max * 1.15) / 2.0) * 2.0; + } + } + + fn previous(&mut self) { + self.legend.previous(); + } + + fn next(&mut self) { + self.legend.next(); + } + + fn unselect(&mut self) { + self.legend.unselect(); + } + + fn set_interpolate(&mut self) { + let interpolate = (1000.0 - self.width as f64) / self.width as f64; + + if interpolate >= 1.0 { + self.interpolate = interpolate as usize; + } else { + self.interpolate = 0; + } + } + + fn zoom_in(&mut self) { + self.width = (self.width as f64 * 0.8) as usize; + self.set_interpolate(); + } + + fn zoom_out(&mut self) { + self.width = (self.width as f64 * 1.25) as usize; + self.set_interpolate(); + } +} + +struct Dashboard { + graphs: Vec, + current: usize, + last: Instant, + interval: u32, + outstanding: bool, +} + +impl Dashboard { + fn new(args: &DashboardArgs) -> Result { + let temps = vec!["temp1".to_string(), "temp2".to_string()]; + let fans = vec!["fan1".to_string(), "fan2".to_string()]; + let current = vec!["amps".to_string()]; + + let graphs = vec![ + Graph::new(&temps, Box::new(TempGraph))?, + Graph::new(&fans, Box::new(FanGraph::new(fans.len())))?, + Graph::new(¤t, Box::new(CurrentGraph))?, + ]; + + Ok(Dashboard { + graphs, + current: 0, + outstanding: true, + last: Instant::now(), + interval: 1000, + }) + } + + fn status(&self) -> Vec<(&str, &str)> { + vec![("Foo", "bar")] + } + + fn need_update(&mut self) -> Result { + Ok(true) + } + + fn update_data(&mut self) { + for graph in self.graphs.iter_mut() { + graph.update_data(); + } + } + + fn up(&mut self) { + self.graphs[self.current].previous(); + } + + fn down(&mut self) { + self.graphs[self.current].next(); + } + + fn esc(&mut self) { + self.graphs[self.current].unselect(); + } + + fn tab(&mut self) { + self.current = (self.current + 1) % self.graphs.len(); + } + + fn zoom_in(&mut self) { + for graph in self.graphs.iter_mut() { + graph.zoom_in(); + } + } + + fn zoom_out(&mut self) { + for graph in self.graphs.iter_mut() { + graph.zoom_out(); + } + } +} + +fn run_dashboard( + terminal: &mut Terminal, + mut dashboard: Dashboard, +) -> Result<()> { + let mut last_tick = Instant::now(); + let tick_rate = Duration::from_millis(100); + + loop { + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + + let update = if crossterm::event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(()), + KeyCode::Char('+') => dashboard.zoom_in(), + KeyCode::Char('-') => dashboard.zoom_out(), + KeyCode::Char('l') => { + // + // ^L -- form feed -- is historically used to clear and + // redraw the screen. And, notably, it is what dtach(1) + // will send when attaching to a dashboard. If we + // see ^L, clear the terminal to force a total redraw. + // + if key.modifiers == KeyModifiers::CONTROL { + terminal.clear()?; + } + } + KeyCode::Up => dashboard.up(), + KeyCode::Down => dashboard.down(), + KeyCode::Esc => dashboard.esc(), + KeyCode::Tab => dashboard.tab(), + _ => {} + } + } + true + } else { + dashboard.need_update()? + }; + + if update { + dashboard.update_data(); + terminal.draw(|f| draw(f, &mut dashboard))?; + } + + last_tick = Instant::now(); + } +} /// /// Runs `omdb mgs dashboard` /// pub(crate) async fn cmd_mgs_dashboard( - _mgs_client: &gateway_client::Client, - _args: &DashboardArgs, + omdb: &crate::Omdb, + log: &slog::Logger, + mgs_args: &crate::mgs::MgsArgs, + args: &DashboardArgs, ) -> Result<(), anyhow::Error> { - anyhow::bail!("not yet"); + let dashboard = Dashboard::new(&args)?; + + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let res = run_dashboard(&mut terminal, dashboard); + + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{err:?}"); + } + + Ok(()) +} + +fn draw_graph(f: &mut Frame, parent: Rect, graph: &mut Graph) { + // + // We want the right panel to be 30 characters wide (a left-justified 20 + // and a right justified 8 + margins), but we don't want it to consume + // more than 80%; calculate accordingly. + // + let r = std::cmp::min((30 * 100) / parent.width, 80); + + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [Constraint::Percentage(100 - r), Constraint::Percentage(r)] + .as_ref(), + ) + .split(parent); + + let x_labels = vec![ + Span::styled( + format!("t-{}", graph.width), + Style::default().add_modifier(Modifier::BOLD), + ), + Span::styled( + format!("t-{}", 1), + Style::default().add_modifier(Modifier::BOLD), + ), + ]; + + let mut datasets = vec![]; + let selected = graph.legend.state.selected(); + + for (ndx, s) in graph.series.iter().enumerate() { + if let Some(selected) = selected { + if ndx != selected { + continue; + } + } + + datasets.push( + Dataset::default() + .name(&s.name) + .marker(symbols::Marker::Braille) + .style(Style::default().fg(s.color)) + .data(&s.data), + ); + } + + let chart = Chart::new(datasets) + .block( + Block::default() + .title(Span::styled( + graph.attributes.label(), + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL), + ) + .x_axis( + Axis::default() + .title(graph.attributes.x_axis_label()) + .style(Style::default().fg(Color::Gray)) + .labels(x_labels) + .bounds([0.0, graph.width as f64]), + ) + .y_axis( + Axis::default() + .title(graph.attributes.y_axis_label()) + .style(Style::default().fg(Color::Gray)) + .labels(vec![ + Span::styled( + graph.attributes.axis_value(graph.bounds[0]), + Style::default().add_modifier(Modifier::BOLD), + ), + Span::styled( + graph.attributes.axis_value(graph.bounds[1]), + Style::default().add_modifier(Modifier::BOLD), + ), + ]) + .bounds(graph.bounds), + ); + + f.render_widget(chart, chunks[0]); + + let mut rows = vec![]; + + for s in &graph.series { + let val = match s.raw.last() { + None | Some(None) => "-".to_string(), + Some(Some(val)) => graph.attributes.legend_value((*val).into()), + }; + + rows.push(ListItem::new(Line::from(vec![ + Span::styled( + format!("{:<20}", s.name), + Style::default().fg(s.color), + ), + Span::styled(format!("{:>8}", val), Style::default().fg(s.color)), + ]))); + } + + let list = List::new(rows) + .block( + Block::default() + .borders(Borders::ALL) + .title(graph.attributes.legend_label()), + ) + .highlight_style( + Style::default() + .bg(Color::LightGreen) + .fg(Color::Black) + .add_modifier(Modifier::BOLD), + ); + + // We can now render the item list + f.render_stateful_widget(list, chunks[1], &mut graph.legend.state); +} + +fn draw_graphs( + f: &mut Frame, + parent: Rect, + dashboard: &mut Dashboard, +) { + let screen = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Ratio(1, 2), + Constraint::Ratio(1, 4), + Constraint::Ratio(1, 4), + ] + .as_ref(), + ) + .split(parent); + + draw_graph(f, screen[0], &mut dashboard.graphs[0]); + draw_graph(f, screen[1], &mut dashboard.graphs[1]); + draw_graph(f, screen[2], &mut dashboard.graphs[2]); +} + +fn draw_status( + f: &mut Frame, + parent: Rect, + status: &[(&str, &str)], +) { + let mut bar = vec![]; + + for i in 0..status.len() { + let s = &status[i]; + + bar.push(Span::styled( + s.0, + Style::default().add_modifier(Modifier::BOLD), + )); + + bar.push(Span::styled( + ": ", + Style::default().add_modifier(Modifier::BOLD), + )); + + bar.push(Span::raw(s.1)); + + if i < status.len() - 1 { + bar.push(Span::raw(" | ")); + } + } + + let text = vec![Line::from(bar)]; + + let para = Paragraph::new(text) + .alignment(Alignment::Right) + .style(Style::default().fg(Color::White).bg(Color::Black)); + + f.render_widget(para, parent); +} + +fn draw(f: &mut Frame, dashboard: &mut Dashboard) { + let size = f.size(); + + let screen = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(1), Constraint::Length(1)].as_ref()) + .split(size); + + draw_graphs(f, screen[0], dashboard); + draw_status(f, screen[1], &dashboard.status()); } From 6f3a70c381a5083869129b9d1a17b33af2dd0731 Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Wed, 3 Jan 2024 06:40:47 +0000 Subject: [PATCH 11/21] wip --- dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs | 157 ++++++++++++------- dev-tools/omdb/src/bin/omdb/mgs/sensors.rs | 55 ++++--- 2 files changed, 133 insertions(+), 79 deletions(-) diff --git a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs index 5e4b49e6d1..06875e2131 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs @@ -30,14 +30,24 @@ use ratatui::{ Frame, Terminal, }; +use crate::mgs::sensors::{ + sensor_metadata, Sensor, SensorId, SensorInput, SensorMetadata, + SensorValues, SensorsArgs, +}; use clap::Args; +use gateway_client::types::MeasurementKind; +use gateway_client::types::SpIdentifier; +use gateway_client::types::SpType; +use multimap::MultiMap; +use std::collections::{HashMap, HashSet}; +use std::fs::File; use std::io; use std::time::{Duration, Instant}; #[derive(Debug, Args)] pub(crate) struct DashboardArgs { #[clap(flatten)] - sensors_args: crate::mgs::sensors::SensorsArgs, + sensors_args: SensorsArgs, } struct StatefulList { @@ -117,14 +127,7 @@ impl Attributes for TempGraph { } } -struct FanGraph(Vec); - -impl FanGraph { - fn new(len: usize) -> Self { - let v = vec![0; len]; - FanGraph(v) - } -} +struct FanGraph; impl Attributes for FanGraph { fn label(&self) -> String { @@ -145,28 +148,6 @@ impl Attributes for FanGraph { fn legend_value(&self, val: f64) -> String { format!("{:.0}", val) } - - fn increase(&mut self, ndx: usize) -> Option { - let current = self.0[ndx]; - let nval = current + 20; - - self.0[ndx] = if nval <= 100 { nval } else { 100 }; - Some(self.0[ndx]) - } - - fn decrease(&mut self, ndx: usize) -> Option { - let current = self.0[ndx]; - let nval = current as i8 - 20; - - self.0[ndx] = if nval >= 0 { nval as u8 } else { 0 }; - Some(self.0[ndx]) - } - - fn clear(&mut self) { - for val in self.0.iter_mut() { - *val = 0; - } - } } struct CurrentGraph; @@ -363,28 +344,63 @@ impl Graph { } struct Dashboard { - graphs: Vec, + graphs: HashMap<(SpIdentifier, MeasurementKind), Graph>, current: usize, + sps: Vec, + sensor_to_graph: HashMap, + current_sp: usize, last: Instant, interval: u32, outstanding: bool, } impl Dashboard { - fn new(args: &DashboardArgs) -> Result { - let temps = vec!["temp1".to_string(), "temp2".to_string()]; - let fans = vec!["fan1".to_string(), "fan2".to_string()]; - let current = vec!["amps".to_string()]; - - let graphs = vec![ - Graph::new(&temps, Box::new(TempGraph))?, - Graph::new(&fans, Box::new(FanGraph::new(fans.len())))?, - Graph::new(¤t, Box::new(CurrentGraph))?, - ]; + fn new( + args: &DashboardArgs, + metadata: std::sync::Arc<&SensorMetadata>, + ) -> Result { + let mut sensor_to_graph = HashMap::new(); + let mut sps = + metadata.sensors_by_sp.keys().map(|m| *m).collect::>(); + let mut graphs = HashMap::new(); + sps.sort(); + + for sp in sps.iter() { + let sp = *sp; + let sensors = metadata.sensors_by_sp.get_vec(&sp).unwrap(); + let mut by_kind = MultiMap::new(); + + for sid in sensors { + let (_, s, _) = metadata.sensors_by_id.get(sid).unwrap(); + by_kind.insert(s.kind, (s.name.clone(), *sid)); + } + + let keys = by_kind.keys().map(|k| *k).collect::>(); + + for k in keys { + let mut v = by_kind.remove(&k).unwrap(); + v.sort(); + + for (ndx, (_, sid)) in v.iter().enumerate() { + sensor_to_graph.insert(*sid, (sp, k, ndx)); + } + + let labels = + v.iter().map(|(n, _)| n.clone()).collect::>(); + + graphs.insert( + (sp, k), + Graph::new(labels.as_slice(), Box::new(TempGraph))?, + ); + } + } Ok(Dashboard { graphs, current: 0, + sps, + sensor_to_graph, + current_sp: 0, outstanding: true, last: Instant::now(), interval: 1000, @@ -400,35 +416,35 @@ impl Dashboard { } fn update_data(&mut self) { - for graph in self.graphs.iter_mut() { + for graph in self.graphs.values_mut() { graph.update_data(); } } fn up(&mut self) { - self.graphs[self.current].previous(); + // self.graphs[self.current].previous(); } fn down(&mut self) { - self.graphs[self.current].next(); + // self.graphs[self.current].next(); } fn esc(&mut self) { - self.graphs[self.current].unselect(); + // self.graphs[self.current].unselect(); } fn tab(&mut self) { - self.current = (self.current + 1) % self.graphs.len(); + // self.current = (self.current + 1) % self.graphs.len(); } fn zoom_in(&mut self) { - for graph in self.graphs.iter_mut() { + for graph in self.graphs.values_mut() { graph.zoom_in(); } } fn zoom_out(&mut self) { - for graph in self.graphs.iter_mut() { + for graph in self.graphs.values_mut() { graph.zoom_out(); } } @@ -493,7 +509,26 @@ pub(crate) async fn cmd_mgs_dashboard( mgs_args: &crate::mgs::MgsArgs, args: &DashboardArgs, ) -> Result<(), anyhow::Error> { - let dashboard = Dashboard::new(&args)?; + let mut input = if let Some(ref input) = args.sensors_args.input { + let file = File::open(input)?; + SensorInput::CsvReader( + csv::Reader::from_reader(file), + csv::Position::new(), + ) + } else { + SensorInput::MgsClient(mgs_args.mgs_client(omdb, log).await?) + }; + + let (metadata, mut values) = + sensor_metadata(&mut input, &args.sensors_args).await?; + + // + // A bit of shenangians to force metadata to be 'static -- which allows + // us to share it with tasks. + // + let metadata = Box::leak(Box::new(metadata)); + let metadata: &_ = metadata; + let metadata = std::sync::Arc::new(metadata); // setup terminal enable_raw_mode()?; @@ -502,6 +537,8 @@ pub(crate) async fn cmd_mgs_dashboard( let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; + let dashboard = Dashboard::new(&args, metadata)?; + let res = run_dashboard(&mut terminal, dashboard); // restore terminal @@ -654,9 +691,23 @@ fn draw_graphs( ) .split(parent); - draw_graph(f, screen[0], &mut dashboard.graphs[0]); - draw_graph(f, screen[1], &mut dashboard.graphs[1]); - draw_graph(f, screen[2], &mut dashboard.graphs[2]); + let sp = dashboard.sps[dashboard.current_sp]; + + draw_graph( + f, + screen[0], + dashboard.graphs.get_mut(&(sp, MeasurementKind::Temperature)).unwrap(), + ); + draw_graph( + f, + screen[1], + dashboard.graphs.get_mut(&(sp, MeasurementKind::Speed)).unwrap(), + ); + draw_graph( + f, + screen[2], + dashboard.graphs.get_mut(&(sp, MeasurementKind::Current)).unwrap(), + ); } fn draw_status( diff --git a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs index 9f5e398f83..7680da8938 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs @@ -21,31 +21,31 @@ use std::time::{Duration, SystemTime}; pub(crate) struct SensorsArgs { /// verbose messages #[clap(long, short)] - verbose: bool, + pub verbose: bool, /// restrict to specified sled(s) #[clap(long, use_value_delimiter = true)] - sleds: Vec, + pub sleds: Vec, /// exclude sleds rather than include them #[clap(long, short)] - exclude: bool, + pub exclude: bool, /// include switches #[clap(long)] - switches: bool, + pub switches: bool, /// include PSC #[clap(long)] - psc: bool, + pub psc: bool, /// print sensors every second #[clap(long, short)] - sleep: bool, + pub sleep: bool, /// parseable output #[clap(long, short)] - parseable: bool, + pub parseable: bool, /// restrict sensors by type of sensor #[clap( @@ -54,7 +54,7 @@ pub(crate) struct SensorsArgs { value_name = "sensor type", use_value_delimiter = true )] - types: Option>, + pub types: Option>, /// restrict sensors by name #[clap( @@ -63,11 +63,11 @@ pub(crate) struct SensorsArgs { value_name = "sensor name", use_value_delimiter = true )] - named: Option>, + pub named: Option>, /// simulate using specified file as input #[clap(long, short)] - input: Option, + pub input: Option, } impl SensorsArgs { @@ -89,9 +89,9 @@ impl SensorsArgs { } #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] -struct Sensor { - name: String, - kind: MeasurementKind, +pub(crate) struct Sensor { + pub name: String, + pub kind: MeasurementKind, } impl Sensor { @@ -152,30 +152,33 @@ impl Sensor { } } -enum SensorInput { +pub(crate) enum SensorInput { MgsClient(gateway_client::Client), CsvReader(csv::Reader, csv::Position), } -#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] -struct SensorId(u32); +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct SensorId(u32); #[derive(Debug)] -struct SensorMetadata { - sensors_by_sensor: MultiMap, - sensors_by_sensor_and_sp: HashMap>, - sensors_by_id: HashMap, - sensors_by_sp: MultiMap, - work_by_sp: HashMap)>>, +pub(crate) struct SensorMetadata { + pub sensors_by_sensor: MultiMap, + pub sensors_by_sensor_and_sp: + HashMap>, + pub sensors_by_id: + HashMap, + pub sensors_by_sp: MultiMap, + pub work_by_sp: + HashMap)>>, } -struct SensorValues { +pub(crate) struct SensorValues { values: HashMap>, time: u64, } #[derive(Clone, Debug, Hash, PartialEq, Eq)] -enum DeviceIdentifier { +pub(crate) enum DeviceIdentifier { Field(usize), Device(String), } @@ -451,7 +454,7 @@ fn sp_info_csv( Ok((rval, time.unwrap())) } -async fn sensor_metadata( +pub(crate) async fn sensor_metadata( input: &mut SensorInput, args: &SensorsArgs, ) -> Result<(SensorMetadata, SensorValues), anyhow::Error> { @@ -685,7 +688,7 @@ fn sp_data_csv( Ok(SensorValues { values, time: time.unwrap() }) } -async fn sensor_data( +pub(crate) async fn sensor_data( input: &mut SensorInput, metadata: std::sync::Arc<&'static SensorMetadata>, ) -> Result { From 2e3e987e82f2765ff489c17b6953821b35384e5f Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Wed, 3 Jan 2024 23:04:52 +0000 Subject: [PATCH 12/21] wip --- dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs | 194 ++++++++++++++----- dev-tools/omdb/src/bin/omdb/mgs/sensors.rs | 4 +- 2 files changed, 149 insertions(+), 49 deletions(-) diff --git a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs index 06875e2131..112e6c9de0 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs @@ -31,8 +31,8 @@ use ratatui::{ }; use crate::mgs::sensors::{ - sensor_metadata, Sensor, SensorId, SensorInput, SensorMetadata, - SensorValues, SensorsArgs, + sensor_data, sensor_metadata, Sensor, SensorId, SensorInput, + SensorMetadata, SensorValues, SensorsArgs, }; use clap::Args; use gateway_client::types::MeasurementKind; @@ -42,7 +42,7 @@ use multimap::MultiMap; use std::collections::{HashMap, HashSet}; use std::fs::File; use std::io; -use std::time::{Duration, Instant}; +use std::time::{Duration, Instant, SystemTime}; #[derive(Debug, Args)] pub(crate) struct DashboardArgs { @@ -156,6 +156,7 @@ impl Attributes for CurrentGraph { fn label(&self) -> String { "Output current".to_string() } + fn legend_label(&self) -> String { "Regulators".to_string() } @@ -173,6 +174,30 @@ impl Attributes for CurrentGraph { } } +struct SensorGraph; + +impl Attributes for SensorGraph { + fn label(&self) -> String { + "Sensor output".to_string() + } + + fn legend_label(&self) -> String { + "Sensors".to_string() + } + + fn y_axis_label(&self) -> String { + "Units".to_string() + } + + fn axis_value(&self, val: f64) -> String { + format!("{:2.2}", val) + } + + fn legend_value(&self, val: f64) -> String { + format!("{:3.2}", val) + } +} + struct Graph { series: Vec, legend: StatefulList, @@ -345,6 +370,7 @@ impl Graph { struct Dashboard { graphs: HashMap<(SpIdentifier, MeasurementKind), Graph>, + sids: HashMap<(SpIdentifier, MeasurementKind), Vec>, current: usize, sps: Vec, sensor_to_graph: HashMap, @@ -352,6 +378,7 @@ struct Dashboard { last: Instant, interval: u32, outstanding: bool, + status: String, } impl Dashboard { @@ -363,6 +390,7 @@ impl Dashboard { let mut sps = metadata.sensors_by_sp.keys().map(|m| *m).collect::>(); let mut graphs = HashMap::new(); + let mut sids = HashMap::new(); sps.sort(); for sp in sps.iter() { @@ -390,13 +418,27 @@ impl Dashboard { graphs.insert( (sp, k), - Graph::new(labels.as_slice(), Box::new(TempGraph))?, + Graph::new( + labels.as_slice(), + match k { + MeasurementKind::Temperature => Box::new(TempGraph), + MeasurementKind::Current => Box::new(CurrentGraph), + MeasurementKind::Speed => Box::new(FanGraph), + _ => Box::new(SensorGraph), + }, + )?, + ); + + sids.insert( + (sp, k), + v.iter().map(|(_, sid)| *sid).collect::>(), ); } } Ok(Dashboard { graphs, + sids, current: 0, sps, sensor_to_graph, @@ -404,11 +446,12 @@ impl Dashboard { outstanding: true, last: Instant::now(), interval: 1000, + status: "Init".to_string(), }) } fn status(&self) -> Vec<(&str, &str)> { - vec![("Foo", "bar")] + vec![("Status", &self.status)] } fn need_update(&mut self) -> Result { @@ -448,56 +491,70 @@ impl Dashboard { graph.zoom_out(); } } + + fn values(&mut self, values: &SensorValues) { + for (graph, sids) in &self.sids { + let mut data = vec![]; + + for sid in sids { + if let Some(value) = values.values.get(&sid) { + data.push(*value); + } else { + data.push(None); + } + } + + let graph = self.graphs.get_mut(graph).unwrap(); + graph.data(data.as_slice()); + } + } } fn run_dashboard( terminal: &mut Terminal, - mut dashboard: Dashboard, -) -> Result<()> { - let mut last_tick = Instant::now(); - let tick_rate = Duration::from_millis(100); - - loop { - let timeout = tick_rate - .checked_sub(last_tick.elapsed()) - .unwrap_or_else(|| Duration::from_secs(0)); - - let update = if crossterm::event::poll(timeout)? { - if let Event::Key(key) = event::read()? { - match key.code { - KeyCode::Char('q') => return Ok(()), - KeyCode::Char('+') => dashboard.zoom_in(), - KeyCode::Char('-') => dashboard.zoom_out(), - KeyCode::Char('l') => { - // - // ^L -- form feed -- is historically used to clear and - // redraw the screen. And, notably, it is what dtach(1) - // will send when attaching to a dashboard. If we - // see ^L, clear the terminal to force a total redraw. - // - if key.modifiers == KeyModifiers::CONTROL { - terminal.clear()?; - } + dashboard: &mut Dashboard, + force_update: bool, +) -> Result { + let update = if crossterm::event::poll(Duration::from_secs(0))? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(true), + KeyCode::Char('+') => dashboard.zoom_in(), + KeyCode::Char('-') => dashboard.zoom_out(), + KeyCode::Char('l') => { + // + // ^L -- form feed -- is historically used to clear and + // redraw the screen. And, notably, it is what dtach(1) + // will send when attaching to a dashboard. If we + // see ^L, clear the terminal to force a total redraw. + // + if key.modifiers == KeyModifiers::CONTROL { + terminal.clear()?; } - KeyCode::Up => dashboard.up(), - KeyCode::Down => dashboard.down(), - KeyCode::Esc => dashboard.esc(), - KeyCode::Tab => dashboard.tab(), - _ => {} } + KeyCode::Up => dashboard.up(), + KeyCode::Down => dashboard.down(), + KeyCode::Esc => dashboard.esc(), + KeyCode::Tab => dashboard.tab(), + _ => {} } - true - } else { - dashboard.need_update()? - }; - - if update { - dashboard.update_data(); - terminal.draw(|f| draw(f, &mut dashboard))?; } + true + } else { + force_update + }; - last_tick = Instant::now(); + if update { + dashboard.update_data(); + terminal.draw(|f| draw(f, dashboard))?; } + + Ok(false) +} + +fn secs() -> Result { + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; + Ok(now.as_secs()) } /// @@ -537,9 +594,52 @@ pub(crate) async fn cmd_mgs_dashboard( let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let dashboard = Dashboard::new(&args, metadata)?; + let mut dashboard = Dashboard::new(&args, metadata.clone())?; + let mut last = secs()?; + let mut update = true; + + let res = 'outer: loop { + match run_dashboard(&mut terminal, &mut dashboard, update) { + Err(err) => { + break Err(err); + } + Ok(true) => { + break Ok(()); + } + _ => {} + } + + update = false; + let now = secs()?; + + if now != last { + let kicked = Instant::now(); + let f = sensor_data(&mut input, metadata.clone()); + last = now; + + while Instant::now().duration_since(kicked).as_millis() < 800 { + tokio::time::sleep(Duration::from_millis(10)).await; + + match run_dashboard(&mut terminal, &mut dashboard, update) { + Err(err) => { + break 'outer Err(err); + } + Ok(true) => { + break 'outer Ok(()); + } + _ => {} + } + } + + let values = f.await?; + dashboard.values(&values); + dashboard.status = format!("{}", values.time); + update = true; + continue; + } - let res = run_dashboard(&mut terminal, dashboard); + tokio::time::sleep(Duration::from_millis(10)).await; + }; // restore terminal disable_raw_mode()?; diff --git a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs index 7680da8938..90b05e3f2e 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs @@ -173,8 +173,8 @@ pub(crate) struct SensorMetadata { } pub(crate) struct SensorValues { - values: HashMap>, - time: u64, + pub values: HashMap>, + pub time: u64, } #[derive(Clone, Debug, Hash, PartialEq, Eq)] From cfa9912279daa22bf331d7a6a88e80d6f0fa25e8 Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Thu, 4 Jan 2024 01:05:12 +0000 Subject: [PATCH 13/21] wip --- dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs | 64 ++++++++++++++++---- dev-tools/omdb/src/bin/omdb/mgs/sensors.rs | 52 +++++++++++++++- 2 files changed, 103 insertions(+), 13 deletions(-) diff --git a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs index 112e6c9de0..dd7244b635 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs @@ -48,6 +48,10 @@ use std::time::{Duration, Instant, SystemTime}; pub(crate) struct DashboardArgs { #[clap(flatten)] sensors_args: SensorsArgs, + + /// simulate real-time with input + #[clap(long)] + simulate_realtime: bool, } struct StatefulList { @@ -436,6 +440,8 @@ impl Dashboard { } } + let status = format!("{:?}", sps[0]); + Ok(Dashboard { graphs, sids, @@ -446,7 +452,7 @@ impl Dashboard { outstanding: true, last: Instant::now(), interval: 1000, - status: "Init".to_string(), + status, }) } @@ -472,6 +478,24 @@ impl Dashboard { // self.graphs[self.current].next(); } + fn left(&mut self) { + self.current_sp = (self.current_sp - 1) % self.sps.len(); + self.status = format!("{:?}", self.sps[self.current_sp]); + } + + fn right(&mut self) { + self.current_sp = (self.current_sp + 1) % self.sps.len(); + self.status = format!("{:?}", self.sps[self.current_sp]); + } + + fn time_left(&mut self) { + // XXX + } + + fn time_right(&mut self) { + // XXX + } + fn esc(&mut self) { // self.graphs[self.current].unselect(); } @@ -521,6 +545,8 @@ fn run_dashboard( KeyCode::Char('q') => return Ok(true), KeyCode::Char('+') => dashboard.zoom_in(), KeyCode::Char('-') => dashboard.zoom_out(), + KeyCode::Char('<') => dashboard.time_left(), + KeyCode::Char('>') => dashboard.time_right(), KeyCode::Char('l') => { // // ^L -- form feed -- is historically used to clear and @@ -534,6 +560,8 @@ fn run_dashboard( } KeyCode::Up => dashboard.up(), KeyCode::Down => dashboard.down(), + KeyCode::Right => dashboard.right(), + KeyCode::Left => dashboard.left(), KeyCode::Esc => dashboard.esc(), KeyCode::Tab => dashboard.tab(), _ => {} @@ -576,7 +604,7 @@ pub(crate) async fn cmd_mgs_dashboard( SensorInput::MgsClient(mgs_args.mgs_client(omdb, log).await?) }; - let (metadata, mut values) = + let (metadata, values) = sensor_metadata(&mut input, &args.sensors_args).await?; // @@ -587,19 +615,34 @@ pub(crate) async fn cmd_mgs_dashboard( let metadata: &_ = metadata; let metadata = std::sync::Arc::new(metadata); + let mut dashboard = Dashboard::new(&args, metadata.clone())?; + let mut last = secs()?; + let mut force = true; + // setup terminal enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - - let mut dashboard = Dashboard::new(&args, metadata.clone())?; - let mut last = secs()?; let mut update = true; + if args.sensors_args.input.is_some() && !args.simulate_realtime { + loop { + let values = sensor_data(&mut input, metadata.clone()).await?; + + if values.time == 0 { + break; + } + + dashboard.values(&values); + } + + update = false; + } + let res = 'outer: loop { - match run_dashboard(&mut terminal, &mut dashboard, update) { + match run_dashboard(&mut terminal, &mut dashboard, force) { Err(err) => { break Err(err); } @@ -609,10 +652,10 @@ pub(crate) async fn cmd_mgs_dashboard( _ => {} } - update = false; + force = false; let now = secs()?; - if now != last { + if update && now != last { let kicked = Instant::now(); let f = sensor_data(&mut input, metadata.clone()); last = now; @@ -620,7 +663,7 @@ pub(crate) async fn cmd_mgs_dashboard( while Instant::now().duration_since(kicked).as_millis() < 800 { tokio::time::sleep(Duration::from_millis(10)).await; - match run_dashboard(&mut terminal, &mut dashboard, update) { + match run_dashboard(&mut terminal, &mut dashboard, force) { Err(err) => { break 'outer Err(err); } @@ -633,8 +676,7 @@ pub(crate) async fn cmd_mgs_dashboard( let values = f.await?; dashboard.values(&values); - dashboard.status = format!("{}", values.time); - update = true; + force = true; continue; } diff --git a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs index 90b05e3f2e..ff2fbd34e2 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs @@ -68,6 +68,14 @@ pub(crate) struct SensorsArgs { /// simulate using specified file as input #[clap(long, short)] pub input: Option, + + /// start time, if using an input file + #[clap(long, value_name = "time", requires = "input")] + pub start: Option, + + /// end time, if using an input file + #[clap(long, value_name = "time", requires = "input")] + pub end: Option, } impl SensorsArgs { @@ -170,6 +178,8 @@ pub(crate) struct SensorMetadata { pub sensors_by_sp: MultiMap, pub work_by_sp: HashMap)>>, + pub start_time: Option, + pub end_time: Option, } pub(crate) struct SensorValues { @@ -409,9 +419,34 @@ fn sp_info_csv( } if time.is_none() { - time = Some(record[0].parse::().or_else(|_| { + let t = record[0].parse::().or_else(|_| { bail!("bad time at line {}", position.line()); - })?); + })?; + + if let Some(start) = args.start { + if t < start { + continue; + } + } + + if let Some(end) = args.end { + if let Some(start) = args.start { + if start > end { + bail!( + "specified start time is later than end time" + ); + } + } + + if t > end { + bail!( + "specified end time ({end}) is earlier \ + than time of earliest record ({t})" + ); + } + } + + time = Some(t); } if let Some(sensor) = Sensor::from_string(&record[1], &record[2]) { @@ -437,6 +472,10 @@ fn sp_info_csv( } } + if time.is_none() { + bail!("no data found"); + } + let mut rval = vec![]; for (field, sp) in sps.iter().enumerate() { @@ -553,6 +592,8 @@ pub(crate) async fn sensor_metadata( sensors_by_id, sensors_by_sp, work_by_sp, + start_time: args.start, + end_time: args.end, }, SensorValues { values, time }, )) @@ -661,6 +702,13 @@ fn sp_data_csv( break; } } else { + if let Some(end) = metadata.end_time { + if now > end { + time = Some(0); + break; + } + } + time = Some(now); } From 7fd03af4c8c4e8519374a15683fcb85820010bec Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Thu, 4 Jan 2024 08:33:31 +0000 Subject: [PATCH 14/21] wip --- Cargo.lock | 5 +- Cargo.toml | 1 + dev-tools/omdb/Cargo.toml | 1 + dev-tools/omdb/src/bin/omdb/mgs.rs | 4 + dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs | 312 ++++++++++++++++--- 5 files changed, 273 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ddadee0135..7d995802a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2026,9 +2026,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.13" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" [[package]] name = "ecdsa" @@ -4887,6 +4887,7 @@ dependencies = [ "csv", "diesel", "dropshot", + "dyn-clone", "expectorate", "futures", "gateway-client", diff --git a/Cargo.toml b/Cargo.toml index 24649d7582..15f9b8f5ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -189,6 +189,7 @@ dns-server = { path = "dns-server" } dns-service-client = { path = "clients/dns-service-client" } dpd-client = { path = "clients/dpd-client" } dropshot = { git = "https://github.com/oxidecomputer/dropshot", branch = "main", features = [ "usdt-probes" ] } +dyn-clone = "1.0.16" either = "1.9.0" expectorate = "1.1.0" fatfs = "0.3.6" diff --git a/dev-tools/omdb/Cargo.toml b/dev-tools/omdb/Cargo.toml index ed47732384..d54e620e1d 100644 --- a/dev-tools/omdb/Cargo.toml +++ b/dev-tools/omdb/Cargo.toml @@ -17,6 +17,7 @@ crucible-agent-client.workspace = true csv.workspace = true diesel.workspace = true dropshot.workspace = true +dyn-clone.workspace = true futures.workspace = true gateway-client.workspace = true gateway-messages.workspace = true diff --git a/dev-tools/omdb/src/bin/omdb/mgs.rs b/dev-tools/omdb/src/bin/omdb/mgs.rs index bb34c5ab5b..1361150ac0 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs.rs @@ -181,6 +181,10 @@ fn sp_type_to_str(s: &SpType) -> &'static str { } } +fn sp_to_string(s: &SpIdentifier) -> String { + format!("{} {}", sp_type_to_str(&s.type_), s.slot) +} + fn show_sp_ids(sp_ids: &[SpIdentifier]) -> Result<(), anyhow::Error> { #[derive(Tabled)] #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] diff --git a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs index dd7244b635..304d0b73f6 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs @@ -17,6 +17,7 @@ use crossterm::{ LeaveAlternateScreen, }, }; +use dyn_clone::DynClone; use ratatui::{ backend::{Backend, CrosstermBackend}, layout::{Alignment, Constraint, Direction, Layout, Rect}, @@ -34,6 +35,7 @@ use crate::mgs::sensors::{ sensor_data, sensor_metadata, Sensor, SensorId, SensorInput, SensorMetadata, SensorValues, SensorsArgs, }; +use crate::mgs::sp_to_string; use clap::Args; use gateway_client::types::MeasurementKind; use gateway_client::types::SpIdentifier; @@ -78,6 +80,10 @@ impl StatefulList { fn unselect(&mut self) { self.state.select(None); } + + fn selected(&self) -> Option { + self.state.selected() + } } struct Series { @@ -87,7 +93,7 @@ struct Series { raw: Vec>, } -trait Attributes { +trait Attributes: DynClone { fn label(&self) -> String; fn legend_label(&self) -> String; fn x_axis_label(&self) -> String { @@ -108,6 +114,9 @@ trait Attributes { fn clear(&mut self) {} } +dyn_clone::clone_trait_object!(Attributes); + +#[derive(Clone)] struct TempGraph; impl Attributes for TempGraph { @@ -131,6 +140,7 @@ impl Attributes for TempGraph { } } +#[derive(Clone)] struct FanGraph; impl Attributes for FanGraph { @@ -154,6 +164,7 @@ impl Attributes for FanGraph { } } +#[derive(Clone)] struct CurrentGraph; impl Attributes for CurrentGraph { @@ -166,7 +177,7 @@ impl Attributes for CurrentGraph { } fn y_axis_label(&self) -> String { - "Amperes".to_string() + "Rails".to_string() } fn axis_value(&self, val: f64) -> String { @@ -178,6 +189,32 @@ impl Attributes for CurrentGraph { } } +#[derive(Clone)] +struct VoltageGraph; + +impl Attributes for VoltageGraph { + fn label(&self) -> String { + "Voltage".to_string() + } + + fn legend_label(&self) -> String { + "Rails".to_string() + } + + fn y_axis_label(&self) -> String { + "Volts".to_string() + } + + fn axis_value(&self, val: f64) -> String { + format!("{:2.2}V", val) + } + + fn legend_value(&self, val: f64) -> String { + format!("{:3.2}V", val) + } +} + +#[derive(Clone)] struct SensorGraph; impl Attributes for SensorGraph { @@ -207,6 +244,7 @@ struct Graph { legend: StatefulList, time: usize, width: usize, + offs: usize, interpolate: usize, bounds: [f64; 2], attributes: Box, @@ -246,18 +284,64 @@ impl Graph { legend: StatefulList { state: ListState::default(), n: all.len() }, time: 0, width: 600, + offs: 0, interpolate: 0, bounds: [20.0, 120.0], attributes: attr, }) } + fn flip(from: &[(&Self, String)], series_ndx: usize) -> Self { + let rep = from[0].0; + let mut series = vec![]; + + let colors = [ + Color::Yellow, + Color::Green, + Color::Magenta, + Color::White, + Color::Red, + Color::LightRed, + Color::Blue, + Color::LightMagenta, + Color::LightYellow, + Color::LightCyan, + Color::LightGreen, + Color::LightBlue, + Color::LightRed, + ]; + + for (ndx, (graph, name)) in from.iter().enumerate() { + series.push(Series { + name: name.clone(), + color: colors[ndx % colors.len()], + data: graph.series[series_ndx].data.clone(), + raw: graph.series[series_ndx].raw.clone(), + }); + } + + Graph { + series, + legend: StatefulList { state: ListState::default(), n: from.len() }, + time: rep.time, + width: rep.width, + offs: rep.offs, + interpolate: rep.interpolate, + bounds: rep.bounds, + attributes: rep.attributes.clone(), + } + } + fn data(&mut self, data: &[Option]) { for (ndx, s) in self.series.iter_mut().enumerate() { s.raw.push(data[ndx]); } self.time += 1; + + if self.offs > 0 { + self.offs += 1; + } } fn update_data(&mut self) { @@ -266,11 +350,11 @@ impl Graph { } for i in 0..self.width { - if self.time < self.width - i { + if self.time < (self.width - i) + self.offs { continue; } - let offs = self.time - (self.width - i); + let offs = self.time - (self.width - i) - self.offs; for (_ndx, s) in &mut self.series.iter_mut().enumerate() { if let Some(datum) = s.raw[offs] { @@ -351,6 +435,10 @@ impl Graph { self.legend.unselect(); } + fn selected(&self) -> Option { + self.legend.selected() + } + fn set_interpolate(&mut self) { let interpolate = (1000.0 - self.width as f64) / self.width as f64; @@ -370,15 +458,31 @@ impl Graph { self.width = (self.width as f64 * 1.25) as usize; self.set_interpolate(); } + + fn time_right(&mut self) { + let delta = (self.width as f64 * 0.25) as usize; + + if delta > self.offs { + self.offs = 0; + } else { + self.offs -= delta; + } + } + + fn time_left(&mut self) { + self.offs += (self.width as f64 * 0.25) as usize; + } } struct Dashboard { graphs: HashMap<(SpIdentifier, MeasurementKind), Graph>, + flipped: HashMap, sids: HashMap<(SpIdentifier, MeasurementKind), Vec>, - current: usize, + kinds: Vec, + selected_kind: usize, sps: Vec, + selected_sp: usize, sensor_to_graph: HashMap, - current_sp: usize, last: Instant, interval: u32, outstanding: bool, @@ -397,6 +501,12 @@ impl Dashboard { let mut sids = HashMap::new(); sps.sort(); + let kinds = vec![ + MeasurementKind::Temperature, + MeasurementKind::Speed, + MeasurementKind::Current, + ]; + for sp in sps.iter() { let sp = *sp; let sensors = metadata.sensors_by_sp.get_vec(&sp).unwrap(); @@ -428,6 +538,7 @@ impl Dashboard { MeasurementKind::Temperature => Box::new(TempGraph), MeasurementKind::Current => Box::new(CurrentGraph), MeasurementKind::Speed => Box::new(FanGraph), + MeasurementKind::Voltage => Box::new(VoltageGraph), _ => Box::new(SensorGraph), }, )?, @@ -440,15 +551,17 @@ impl Dashboard { } } - let status = format!("{:?}", sps[0]); + let status = sp_to_string(&sps[0]); Ok(Dashboard { graphs, + flipped: HashMap::new(), sids, - current: 0, + kinds, + selected_kind: 0, sps, sensor_to_graph, - current_sp: 0, + selected_sp: 0, outstanding: true, last: Instant::now(), interval: 1000, @@ -468,52 +581,151 @@ impl Dashboard { for graph in self.graphs.values_mut() { graph.update_data(); } + + for graph in self.flipped.values_mut() { + graph.update_data(); + } } fn up(&mut self) { - // self.graphs[self.current].previous(); + let selected_kind = self.kinds[self.selected_kind]; + + if let Some(flipped) = self.flipped.get_mut(&selected_kind) { + flipped.previous(); + return; + } + + for sp in &self.sps { + self.graphs.get_mut(&(*sp, selected_kind)).unwrap().previous(); + } } fn down(&mut self) { - // self.graphs[self.current].next(); + let selected_kind = self.kinds[self.selected_kind]; + + if let Some(flipped) = self.flipped.get_mut(&selected_kind) { + flipped.next(); + return; + } + + for sp in &self.sps { + self.graphs.get_mut(&(*sp, selected_kind)).unwrap().next(); + } } fn left(&mut self) { - self.current_sp = (self.current_sp - 1) % self.sps.len(); - self.status = format!("{:?}", self.sps[self.current_sp]); + if self.selected_sp == 0 { + self.selected_sp = self.sps.len() - 1; + } else { + self.selected_sp -= 1; + } + + self.status = sp_to_string(&self.sps[self.selected_sp]); } fn right(&mut self) { - self.current_sp = (self.current_sp + 1) % self.sps.len(); - self.status = format!("{:?}", self.sps[self.current_sp]); + self.selected_sp = (self.selected_sp + 1) % self.sps.len(); + self.status = sp_to_string(&self.sps[self.selected_sp]); } fn time_left(&mut self) { - // XXX + for graph in self.graphs.values_mut() { + graph.time_left(); + } + + for graph in self.flipped.values_mut() { + graph.time_left(); + } } fn time_right(&mut self) { - // XXX + for graph in self.graphs.values_mut() { + graph.time_right(); + } + + for graph in self.flipped.values_mut() { + graph.time_right(); + } } fn esc(&mut self) { - // self.graphs[self.current].unselect(); + let selected_kind = self.kinds[self.selected_kind]; + + if let Some(flipped) = self.flipped.get_mut(&selected_kind) { + flipped.unselect(); + return; + } + + for sp in &self.sps { + self.graphs.get_mut(&(*sp, selected_kind)).unwrap().unselect(); + } + } + + fn flip(&mut self) { + let selected_kind = self.kinds[self.selected_kind]; + + if let Some(_) = self.flipped.remove(&selected_kind) { + return; + } + + let sp = self.sps[self.selected_sp]; + + let graph = self.graphs.get(&(sp, selected_kind)).unwrap(); + + if let Some(ndx) = graph.selected() { + let mut from = vec![]; + + for sp in &self.sps { + // XXX make sure types match + from.push(( + self.graphs.get(&(*sp, selected_kind)).unwrap(), + sp_to_string(sp), + )); + } + + self.flipped + .insert(selected_kind, Graph::flip(from.as_slice(), ndx)); + } } fn tab(&mut self) { - // self.current = (self.current + 1) % self.graphs.len(); + self.selected_kind = (self.selected_kind + 1) % self.kinds.len(); } fn zoom_in(&mut self) { for graph in self.graphs.values_mut() { graph.zoom_in(); } + + for graph in self.flipped.values_mut() { + graph.zoom_in(); + } } fn zoom_out(&mut self) { for graph in self.graphs.values_mut() { graph.zoom_out(); } + + for graph in self.flipped.values_mut() { + graph.zoom_out(); + } + } + + fn gap(&mut self, length: u64) { + let mut gap: Vec> = vec![]; + + for (graph, sids) in &self.sids { + while gap.len() < sids.len() { + gap.push(None); + } + + let graph = self.graphs.get_mut(graph).unwrap(); + + for _ in 0..length { + graph.data(&gap[0..sids.len()]); + } + } } fn values(&mut self, values: &SensorValues) { @@ -547,6 +759,7 @@ fn run_dashboard( KeyCode::Char('-') => dashboard.zoom_out(), KeyCode::Char('<') => dashboard.time_left(), KeyCode::Char('>') => dashboard.time_right(), + KeyCode::Char('!') => dashboard.flip(), KeyCode::Char('l') => { // // ^L -- form feed -- is historically used to clear and @@ -616,17 +829,12 @@ pub(crate) async fn cmd_mgs_dashboard( let metadata = std::sync::Arc::new(metadata); let mut dashboard = Dashboard::new(&args, metadata.clone())?; - let mut last = secs()?; + let mut last = values.time; let mut force = true; - - // setup terminal - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; let mut update = true; + dashboard.values(&values); + if args.sensors_args.input.is_some() && !args.simulate_realtime { loop { let values = sensor_data(&mut input, metadata.clone()).await?; @@ -635,12 +843,24 @@ pub(crate) async fn cmd_mgs_dashboard( break; } + if values.time != last + 1 { + dashboard.gap(values.time - last - 1); + } + + last = values.time; dashboard.values(&values); } update = false; } + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + let res = 'outer: loop { match run_dashboard(&mut terminal, &mut dashboard, force) { Err(err) => { @@ -701,11 +921,11 @@ pub(crate) async fn cmd_mgs_dashboard( fn draw_graph(f: &mut Frame, parent: Rect, graph: &mut Graph) { // - // We want the right panel to be 30 characters wide (a left-justified 20 + // We want the right panel to be 31 characters wide (a left-justified 20 // and a right justified 8 + margins), but we don't want it to consume // more than 80%; calculate accordingly. // - let r = std::cmp::min((30 * 100) / parent.width, 80); + let r = std::cmp::min((31 * 100) / parent.width, 80); let chunks = Layout::default() .direction(Direction::Horizontal) @@ -717,11 +937,11 @@ fn draw_graph(f: &mut Frame, parent: Rect, graph: &mut Graph) { let x_labels = vec![ Span::styled( - format!("t-{}", graph.width), + format!("t-{}", graph.width + graph.offs), Style::default().add_modifier(Modifier::BOLD), ), Span::styled( - format!("t-{}", 1), + format!("t-{}", graph.offs + 1), Style::default().add_modifier(Modifier::BOLD), ), ]; @@ -833,23 +1053,19 @@ fn draw_graphs( ) .split(parent); - let sp = dashboard.sps[dashboard.current_sp]; - - draw_graph( - f, - screen[0], - dashboard.graphs.get_mut(&(sp, MeasurementKind::Temperature)).unwrap(), - ); - draw_graph( - f, - screen[1], - dashboard.graphs.get_mut(&(sp, MeasurementKind::Speed)).unwrap(), - ); - draw_graph( - f, - screen[2], - dashboard.graphs.get_mut(&(sp, MeasurementKind::Current)).unwrap(), - ); + let sp = dashboard.sps[dashboard.selected_sp]; + + for (i, k) in dashboard.kinds.iter().enumerate() { + if let Some(graph) = dashboard.flipped.get_mut(k) { + draw_graph(f, screen[i], graph); + } else { + draw_graph( + f, + screen[i], + dashboard.graphs.get_mut(&(sp, *k)).unwrap(), + ); + } + } } fn draw_status( From 28497c86d5b338f1852005ff42029b5a4cae1476 Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Fri, 5 Jan 2024 15:55:37 +0000 Subject: [PATCH 15/21] wip --- dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs | 38 +++++++++++--------- dev-tools/omdb/src/bin/omdb/mgs/sensors.rs | 24 ++++++++++++- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs index 304d0b73f6..162617ca31 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs @@ -5,6 +5,7 @@ //! Code for the MGS dashboard subcommand use anyhow::{anyhow, bail, Result}; +use chrono::NaiveDateTime; use clap::{CommandFactory, Parser}; use crossterm::{ event::{ @@ -589,30 +590,46 @@ impl Dashboard { fn up(&mut self) { let selected_kind = self.kinds[self.selected_kind]; + let type_ = self.sps[self.selected_sp].type_; if let Some(flipped) = self.flipped.get_mut(&selected_kind) { flipped.previous(); return; } - for sp in &self.sps { + for sp in self.sps.iter().filter(|&s| s.type_ == type_) { self.graphs.get_mut(&(*sp, selected_kind)).unwrap().previous(); } } fn down(&mut self) { let selected_kind = self.kinds[self.selected_kind]; + let type_ = self.sps[self.selected_sp].type_; if let Some(flipped) = self.flipped.get_mut(&selected_kind) { flipped.next(); return; } - for sp in &self.sps { + for sp in self.sps.iter().filter(|&s| s.type_ == type_) { self.graphs.get_mut(&(*sp, selected_kind)).unwrap().next(); } } + fn esc(&mut self) { + let selected_kind = self.kinds[self.selected_kind]; + let type_ = self.sps[self.selected_sp].type_; + + if let Some(flipped) = self.flipped.get_mut(&selected_kind) { + flipped.unselect(); + return; + } + + for sp in self.sps.iter().filter(|&s| s.type_ == type_) { + self.graphs.get_mut(&(*sp, selected_kind)).unwrap().unselect(); + } + } + fn left(&mut self) { if self.selected_sp == 0 { self.selected_sp = self.sps.len() - 1; @@ -648,21 +665,9 @@ impl Dashboard { } } - fn esc(&mut self) { - let selected_kind = self.kinds[self.selected_kind]; - - if let Some(flipped) = self.flipped.get_mut(&selected_kind) { - flipped.unselect(); - return; - } - - for sp in &self.sps { - self.graphs.get_mut(&(*sp, selected_kind)).unwrap().unselect(); - } - } - fn flip(&mut self) { let selected_kind = self.kinds[self.selected_kind]; + let type_ = self.sps[self.selected_sp].type_; if let Some(_) = self.flipped.remove(&selected_kind) { return; @@ -675,8 +680,7 @@ impl Dashboard { if let Some(ndx) = graph.selected() { let mut from = vec![]; - for sp in &self.sps { - // XXX make sure types match + for sp in self.sps.iter().filter(|&s| s.type_ == type_) { from.push(( self.graphs.get(&(*sp, selected_kind)).unwrap(), sp_to_string(sp), diff --git a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs index ff2fbd34e2..30f199e6ef 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs @@ -460,7 +460,29 @@ fn sp_info_csv( if let Some(sp) = sp { let value = match record[ndx + len].parse::() { Ok(value) => Some(value), - _ => None, + _ => { + // + // We want to distinguish between the device + // having an error ("X") and it being absent + // ("-"); if it's absent, we don't want to add + // it at all. + // + match &record[ndx + len] { + "X" => {} + "-" => continue, + _ => { + bail!( + "line {}: unrecognized value \ + \"{}\" in field {}", + position.line(), + record[ndx + len].to_string(), + ndx + len + ); + } + } + + None + } }; by_sp.insert(sp, (sensor.clone(), value)); From 6e7908ebc5954c82e4e7f6b8c8518809ce5df96e Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Fri, 5 Jan 2024 18:48:43 +0000 Subject: [PATCH 16/21] wip --- dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs | 42 +++++++++++++++++--- dev-tools/omdb/src/bin/omdb/mgs/sensors.rs | 17 +++++++- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs index 162617ca31..bb38e3b9d2 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs @@ -5,7 +5,7 @@ //! Code for the MGS dashboard subcommand use anyhow::{anyhow, bail, Result}; -use chrono::NaiveDateTime; +use chrono::{DateTime, Local, Offset, TimeZone}; use clap::{CommandFactory, Parser}; use crossterm::{ event::{ @@ -488,6 +488,7 @@ struct Dashboard { interval: u32, outstanding: bool, status: String, + time: u64, } impl Dashboard { @@ -567,6 +568,7 @@ impl Dashboard { last: Instant::now(), interval: 1000, status, + time: secs()?, }) } @@ -747,6 +749,8 @@ impl Dashboard { let graph = self.graphs.get_mut(graph).unwrap(); graph.data(data.as_slice()); } + + self.time = values.time; } } @@ -923,7 +927,12 @@ pub(crate) async fn cmd_mgs_dashboard( Ok(()) } -fn draw_graph(f: &mut Frame, parent: Rect, graph: &mut Graph) { +fn draw_graph( + f: &mut Frame, + parent: Rect, + graph: &mut Graph, + now: u64, +) { // // We want the right panel to be 31 characters wide (a left-justified 20 // and a right justified 8 + margins), but we don't want it to consume @@ -939,13 +948,32 @@ fn draw_graph(f: &mut Frame, parent: Rect, graph: &mut Graph) { ) .split(parent); + let latest = now as i64 - graph.offs as i64; + let earliest = Local.timestamp(latest - graph.width as i64, 0); + let latest = Local.timestamp(latest, 0); + let fmt = "%Y-%m-%d %H:%M:%S"; + + let tz_offset = earliest.offset().fix().local_minus_utc(); + let tz = if tz_offset != 0 { + let hours = tz_offset / 3600; + let minutes = (tz_offset % 3600) / 60; + + if minutes != 0 { + format!("Z{:+}:{:02}", hours, minutes.abs()) + } else { + format!("Z{:+}", hours) + } + } else { + "Z".to_string() + }; + let x_labels = vec![ Span::styled( - format!("t-{}", graph.width + graph.offs), + format!("{}{}", earliest.format(fmt), tz), Style::default().add_modifier(Modifier::BOLD), ), Span::styled( - format!("t-{}", graph.offs + 1), + format!("{}{}", latest.format(fmt), tz), Style::default().add_modifier(Modifier::BOLD), ), ]; @@ -985,7 +1013,8 @@ fn draw_graph(f: &mut Frame, parent: Rect, graph: &mut Graph) { .title(graph.attributes.x_axis_label()) .style(Style::default().fg(Color::Gray)) .labels(x_labels) - .bounds([0.0, graph.width as f64]), + .bounds([0.0, graph.width as f64]) + .labels_alignment(Alignment::Right), ) .y_axis( Axis::default() @@ -1061,12 +1090,13 @@ fn draw_graphs( for (i, k) in dashboard.kinds.iter().enumerate() { if let Some(graph) = dashboard.flipped.get_mut(k) { - draw_graph(f, screen[i], graph); + draw_graph(f, screen[i], graph, dashboard.time); } else { draw_graph( f, screen[i], dashboard.graphs.get_mut(&(sp, *k)).unwrap(), + dashboard.time, ); } } diff --git a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs index 30f199e6ef..5bae0a4590 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs @@ -76,6 +76,15 @@ pub(crate) struct SensorsArgs { /// end time, if using an input file #[clap(long, value_name = "time", requires = "input")] pub end: Option, + + /// duration, if using an input file + #[clap( + long, + value_name = "seconds", + requires = "input", + conflicts_with = "end" + )] + pub duration: Option, } impl SensorsArgs { @@ -615,7 +624,13 @@ pub(crate) async fn sensor_metadata( sensors_by_sp, work_by_sp, start_time: args.start, - end_time: args.end, + end_time: match args.end { + Some(end) => Some(end), + None => match args.duration { + Some(duration) => Some(time + duration), + None => None, + }, + }, }, SensorValues { values, time }, )) From a7d68e837119fe426b742016efaeb1e98204e69a Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Fri, 5 Jan 2024 18:52:49 +0000 Subject: [PATCH 17/21] wip --- dev-tools/omdb/src/bin/omdb/mgs/sensors.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs index 5bae0a4590..cfc248bf7a 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs @@ -25,7 +25,7 @@ pub(crate) struct SensorsArgs { /// restrict to specified sled(s) #[clap(long, use_value_delimiter = true)] - pub sleds: Vec, + pub sled: Vec, /// exclude sleds rather than include them #[clap(long, short)] @@ -91,8 +91,8 @@ impl SensorsArgs { fn matches_sp(&self, sp: &SpIdentifier) -> bool { match sp.type_ { SpType::Sled => { - let matched = if self.sleds.len() > 0 { - self.sleds.iter().find(|&&v| v == sp.slot).is_some() + let matched = if self.sled.len() > 0 { + self.sled.iter().find(|&&v| v == sp.slot).is_some() } else { true }; From fc4efc9abfc4aaa54594e9d7450583da660c3537 Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Thu, 18 Jan 2024 06:29:56 +0000 Subject: [PATCH 18/21] add latency tracking --- Cargo.lock | 2 +- dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs | 8 +- dev-tools/omdb/src/bin/omdb/mgs/sensors.rs | 90 +++++++++++++++----- 3 files changed, 71 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e86ad342d..cd5975ff0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4874,7 +4874,7 @@ dependencies = [ "async-bb8-diesel", "chrono", "clap 4.4.3", - "crossterm 0.27.0", + "crossterm", "crucible-agent-client", "csv", "diesel", diff --git a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs index 3fdfbd3b5a..daed47fa6c 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs @@ -32,8 +32,8 @@ use ratatui::{ }; use crate::mgs::sensors::{ - sensor_data, sensor_metadata, SensorId, SensorInput, - SensorMetadata, SensorValues, SensorsArgs, + sensor_data, sensor_metadata, SensorId, SensorInput, SensorMetadata, + SensorValues, SensorsArgs, }; use crate::mgs::sp_to_string; use clap::Args; @@ -486,9 +486,7 @@ struct Dashboard { } impl Dashboard { - fn new( - metadata: &'static SensorMetadata, - ) -> Result { + fn new(metadata: &'static SensorMetadata) -> Result { let mut sps = metadata.sensors_by_sp.keys().map(|m| *m).collect::>(); let mut graphs = HashMap::new(); diff --git a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs index c8f7c3eddc..9c5399ea64 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs @@ -15,7 +15,7 @@ use gateway_client::types::SpType; use multimap::MultiMap; use std::collections::{HashMap, HashSet}; use std::fs::File; -use std::time::SystemTime; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; #[derive(Debug, Args)] pub(crate) struct SensorsArgs { @@ -47,6 +47,10 @@ pub(crate) struct SensorsArgs { #[clap(long, short)] pub parseable: bool, + /// show latencies + #[clap(long)] + pub show_latencies: bool, + /// restrict sensors by type of sensor #[clap( long, @@ -192,8 +196,15 @@ pub(crate) struct SensorMetadata { pub end_time: Option, } +struct SensorSpInfo { + info: Vec<(SpIdentifier, SpInfo)>, + time: u64, + latencies: Option>, +} + pub(crate) struct SensorValues { pub values: HashMap>, + pub latencies: Option>, pub time: u64, } @@ -279,8 +290,9 @@ async fn sp_info( async fn sp_info_mgs( mgs_client: &gateway_client::Client, args: &SensorsArgs, -) -> Result<(Vec<(SpIdentifier, SpInfo)>, u64), anyhow::Error> { +) -> Result { let mut rval = vec![]; + let mut latencies = HashMap::new(); // // First, get all of the SPs that we can see via Ignition @@ -330,14 +342,16 @@ async fn sp_info_mgs( for (sp_id, handle) in handles { match handle.await.unwrap() { Ok(info) => { + let l0 = info.timestamps[1].duration_since(info.timestamps[0]); + let l1 = info.timestamps[2].duration_since(info.timestamps[1]); + if args.verbose { eprintln!( - "mgs: latencies for {sp_id:?}: {:.1?} {:.1?}", - info.timestamps[2].duration_since(info.timestamps[1]), - info.timestamps[1].duration_since(info.timestamps[0]) + "mgs: latencies for {sp_id:?}: {l1:.1?} {l0:.1?}", ); } + latencies.insert(sp_id, l0 + l1); rval.push((sp_id, info)); } @@ -351,17 +365,18 @@ async fn sp_info_mgs( eprintln!("total discovery time {:?}", now.elapsed()); } - Ok(( - rval, - SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?.as_secs(), - )) + Ok(SensorSpInfo { + info: rval, + time: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(), + latencies: Some(latencies), + }) } fn sp_info_csv( reader: &mut csv::Reader, position: &mut csv::Position, args: &SensorsArgs, -) -> Result<(Vec<(SpIdentifier, SpInfo)>, u64), anyhow::Error> { +) -> Result { let mut sps = vec![]; let headers = reader.headers()?; @@ -522,7 +537,7 @@ fn sp_info_csv( } } - Ok((rval, time.unwrap())) + Ok(SensorSpInfo { info: rval, time: time.unwrap(), latencies: None }) } pub(crate) async fn sensor_metadata( @@ -550,7 +565,7 @@ pub(crate) async fn sensor_metadata( None }; - let (info, time) = match input { + let info = match input { SensorInput::MgsClient(ref mgs_client) => { sp_info_mgs(mgs_client, args).await? } @@ -567,8 +582,9 @@ pub(crate) async fn sensor_metadata( let mut work_by_sp = HashMap::new(); let mut current = 0; + let time = info.time; - for (sp_id, info) in info { + for (sp_id, info) in info.info { let mut sp_work = vec![]; for (device, sensors) in info.devices { @@ -633,7 +649,7 @@ pub(crate) async fn sensor_metadata( }, }, }, - SensorValues { values, time }, + SensorValues { values, time, latencies: info.latencies }, )) } @@ -641,10 +657,12 @@ async fn sp_read_sensors( mgs_client: &gateway_client::Client, id: &SpIdentifier, metadata: &SensorMetadata, -) -> Result)>, anyhow::Error> { +) -> Result<(Vec<(SensorId, Option)>, Duration), anyhow::Error> { let work = metadata.work_by_sp.get(id).unwrap(); let mut rval = vec![]; + let start = std::time::Instant::now(); + for (component, ids) in work.iter() { for (value, id) in mgs_client .sp_component_get(id.type_, id.slot, component.device()) @@ -669,7 +687,7 @@ async fn sp_read_sensors( } } - Ok(rval) + Ok((rval, std::time::Instant::now().duration_since(start))) } async fn sp_data_mgs( @@ -677,6 +695,7 @@ async fn sp_data_mgs( metadata: &'static SensorMetadata, ) -> Result { let mut values = HashMap::new(); + let mut latencies = HashMap::new(); let mut handles = vec![]; for sp_id in metadata.sensors_by_sp.keys() { @@ -687,11 +706,13 @@ async fn sp_data_mgs( sp_read_sensors(&mgs_client, &id, metadata).await }); - handles.push(handle); + handles.push((id, handle)); } - for handle in handles { - let rval = handle.await.unwrap()?; + for (id, handle) in handles { + let (rval, latency) = handle.await.unwrap()?; + + latencies.insert(id, latency); for (id, value) in rval { values.insert(id, value); @@ -700,9 +721,8 @@ async fn sp_data_mgs( Ok(SensorValues { values, - time: SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH)? - .as_secs(), + latencies: Some(latencies), + time: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(), }) } @@ -770,7 +790,7 @@ fn sp_data_csv( } } - Ok(SensorValues { values, time: time.unwrap() }) + Ok(SensorValues { values, latencies: None, time: time.unwrap() }) } pub(crate) async fn sensor_data( @@ -855,6 +875,14 @@ pub(crate) async fn cmd_mgs_sensors( } }; + let print_latency = |now: u64| { + if !args.parseable { + print!("{:20} ", "LATENCY"); + } else { + print!("{now},{},{}", "LATENCY", "latency"); + } + }; + let mut wakeup = tokio::time::Instant::now() + tokio::time::Duration::from_millis(1000); @@ -886,6 +914,22 @@ pub(crate) async fn cmd_mgs_sensors( println!(); } + if args.show_latencies { + if let Some(latencies) = values.latencies { + print_latency(values.time); + + for sp in &sps { + print_value(if let Some(latency) = latencies.get(sp) { + format!("{}ms", latency.as_millis()) + } else { + "?".to_string() + }); + } + } + + println!(); + } + if !args.sleep { if args.input.is_none() { break; From 651ceac2df892cdba16abde9b3991ab6b2530614 Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Sat, 3 Feb 2024 02:45:56 +0000 Subject: [PATCH 19/21] rust fmt --- dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs index 1d147b719c..02fa606137 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs @@ -904,12 +904,7 @@ pub(crate) async fn cmd_mgs_dashboard( Ok(()) } -fn draw_graph( - f: &mut Frame, - parent: Rect, - graph: &mut Graph, - now: u64, -) { +fn draw_graph(f: &mut Frame, parent: Rect, graph: &mut Graph, now: u64) { // // We want the right panel to be 31 characters wide (a left-justified 20 // and a right justified 8 + margins), but we don't want it to consume @@ -1046,11 +1041,7 @@ fn draw_graph( f.render_stateful_widget(list, chunks[1], &mut graph.legend.state); } -fn draw_graphs( - f: &mut Frame, - parent: Rect, - dashboard: &mut Dashboard, -) { +fn draw_graphs(f: &mut Frame, parent: Rect, dashboard: &mut Dashboard) { let screen = Layout::default() .direction(Direction::Vertical) .constraints( @@ -1079,11 +1070,7 @@ fn draw_graphs( } } -fn draw_status( - f: &mut Frame, - parent: Rect, - status: &[(&str, &str)], -) { +fn draw_status(f: &mut Frame, parent: Rect, status: &[(&str, &str)]) { let mut bar = vec![]; for i in 0..status.len() { From 9a194c3cfc57baaa74653bbc53e39e93fab9a71a Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Sun, 4 Feb 2024 18:10:09 +0000 Subject: [PATCH 20/21] fix test --- dev-tools/omdb/tests/usage_errors.out | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev-tools/omdb/tests/usage_errors.out b/dev-tools/omdb/tests/usage_errors.out index 2790b0ef83..7688372984 100644 --- a/dev-tools/omdb/tests/usage_errors.out +++ b/dev-tools/omdb/tests/usage_errors.out @@ -270,7 +270,9 @@ Debug a specific Management Gateway Service instance Usage: omdb mgs [OPTIONS] Commands: + dashboard Dashboard of SPs inventory Show information about devices and components visible to MGS + sensors Show information about sensors, as gleaned by MGS help Print this message or the help of the given subcommand(s) Options: From 3590a6796b4518ca8625db84494a107446e564bc Mon Sep 17 00:00:00 2001 From: Bryan Cantrill Date: Mon, 5 Feb 2024 23:23:07 +0000 Subject: [PATCH 21/21] code review comments from John --- dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs | 64 +++++++-------- dev-tools/omdb/src/bin/omdb/mgs/sensors.rs | 86 ++++++++++---------- 2 files changed, 72 insertions(+), 78 deletions(-) diff --git a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs index 02fa606137..20d651bfdf 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/dashboard.rs @@ -4,7 +4,7 @@ //! Code for the MGS dashboard subcommand -use anyhow::Result; +use anyhow::{Context, Result}; use chrono::{Local, Offset, TimeZone}; use crossterm::{ event::{ @@ -486,9 +486,9 @@ struct Dashboard { } impl Dashboard { - fn new(metadata: &'static SensorMetadata) -> Result { + fn new(metadata: &SensorMetadata) -> Result { let mut sps = - metadata.sensors_by_sp.keys().map(|m| *m).collect::>(); + metadata.sensors_by_sp.keys().copied().collect::>(); let mut graphs = HashMap::new(); let mut sids = HashMap::new(); sps.sort(); @@ -499,8 +499,7 @@ impl Dashboard { MeasurementKind::Current, ]; - for sp in sps.iter() { - let sp = *sp; + for &sp in sps.iter() { let sensors = metadata.sensors_by_sp.get_vec(&sp).unwrap(); let mut by_kind = MultiMap::new(); @@ -509,7 +508,7 @@ impl Dashboard { by_kind.insert(s.kind, (s.name.clone(), *sid)); } - let keys = by_kind.keys().map(|k| *k).collect::>(); + let keys = by_kind.keys().copied().collect::>(); for k in keys { let mut v = by_kind.remove(&k).unwrap(); @@ -649,7 +648,7 @@ impl Dashboard { let selected_kind = self.kinds[self.selected_kind]; let type_ = self.sps[self.selected_sp].type_; - if let Some(_) = self.flipped.remove(&selected_kind) { + if self.flipped.remove(&selected_kind).is_some() { return; } @@ -717,7 +716,7 @@ impl Dashboard { let mut data = vec![]; for sid in sids { - if let Some(value) = values.values.get(&sid) { + if let Some(value) = values.values.get(sid) { data.push(*value); } else { data.push(None); @@ -794,7 +793,8 @@ pub(crate) async fn cmd_mgs_dashboard( args: &DashboardArgs, ) -> Result<(), anyhow::Error> { let mut input = if let Some(ref input) = args.sensors_args.input { - let file = File::open(input)?; + let file = File::open(input) + .with_context(|| format!("failed to open {input}"))?; SensorInput::CsvReader( csv::Reader::from_reader(file), csv::Position::new(), @@ -806,14 +806,7 @@ pub(crate) async fn cmd_mgs_dashboard( let (metadata, values) = sensor_metadata(&mut input, &args.sensors_args).await?; - // - // A bit of shenangians to force metadata to be 'static -- which allows - // us to share it with tasks. - // - let metadata = Box::leak(Box::new(metadata)); - let metadata: &_ = metadata; - - let mut dashboard = Dashboard::new(metadata)?; + let mut dashboard = Dashboard::new(&metadata)?; let mut last = values.time; let mut force = true; let mut update = true; @@ -822,7 +815,7 @@ pub(crate) async fn cmd_mgs_dashboard( if args.sensors_args.input.is_some() && !args.simulate_realtime { loop { - let values = sensor_data(&mut input, metadata).await?; + let values = sensor_data(&mut input, &metadata).await?; if values.time == 0 { break; @@ -848,38 +841,38 @@ pub(crate) async fn cmd_mgs_dashboard( let res = 'outer: loop { match run_dashboard(&mut terminal, &mut dashboard, force) { - Err(err) => { - break Err(err); - } - Ok(true) => { - break Ok(()); - } + Err(err) => break Err(err), + Ok(true) => break Ok(()), _ => {} } force = false; - let now = secs()?; + + let now = match secs() { + Err(err) => break Err(err), + Ok(now) => now, + }; if update && now != last { let kicked = Instant::now(); - let f = sensor_data(&mut input, metadata); + let f = sensor_data(&mut input, &metadata); last = now; while Instant::now().duration_since(kicked).as_millis() < 800 { tokio::time::sleep(Duration::from_millis(10)).await; match run_dashboard(&mut terminal, &mut dashboard, force) { - Err(err) => { - break 'outer Err(err); - } - Ok(true) => { - break 'outer Ok(()); - } + Err(err) => break 'outer Err(err), + Ok(true) => break 'outer Ok(()), _ => {} } } - let values = f.await?; + let values = match f.await { + Err(err) => break Err(err), + Ok(v) => v, + }; + dashboard.values(&values); force = true; continue; @@ -923,6 +916,11 @@ fn draw_graph(f: &mut Frame, parent: Rect, graph: &mut Graph, now: u64) { let latest = now as i64 - graph.offs as i64; let earliest = Local.timestamp_opt(latest - graph.width as i64, 0).unwrap(); let latest = Local.timestamp_opt(latest, 0).unwrap(); + + // + // We want a format that preserves horizontal real estate just a tad more + // than .to_rfc3339_opts()... + // let fmt = "%Y-%m-%d %H:%M:%S"; let tz_offset = earliest.offset().fix().local_minus_utc(); diff --git a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs index 9c5399ea64..d00bebd96c 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs @@ -15,6 +15,7 @@ use gateway_client::types::SpType; use multimap::MultiMap; use std::collections::{HashMap, HashSet}; use std::fs::File; +use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; #[derive(Debug, Args)] @@ -95,8 +96,8 @@ impl SensorsArgs { fn matches_sp(&self, sp: &SpIdentifier) -> bool { match sp.type_ { SpType::Sled => { - let matched = if self.sled.len() > 0 { - self.sled.iter().any(|&v| v == sp.slot) + let matched = if !self.sled.is_empty() { + self.sled.contains(&sp.slot) } else { true }; @@ -132,6 +133,11 @@ impl Sensor { } else { match self.kind { MeasurementKind::Speed => { + // + // This space is deliberate: other units (°C, V, A) look + // more natural when directly attached to their value -- + // but RPM looks decidedly unnatural without a space. + // format!("{value:0} RPM") } _ => { @@ -165,11 +171,7 @@ impl Sensor { _ => None, }; - if let Some(kind) = k { - Some(Sensor { name: name.to_string(), kind }) - } else { - None - } + k.map(|kind| Sensor { name: name.to_string(), kind }) } } @@ -208,6 +210,13 @@ pub(crate) struct SensorValues { pub time: u64, } +/// +/// We identify a device as either a physical device (i.e., when connecting +/// to MGS), or as a field in the CSV header (i.e., when processing data +/// postmortem. It's handy to have this as enum to allow most of the code +/// to be agnostic to the underlying source, but callers of ['device'] and +/// ['field'] are expected to know which of these they're dealing with. +/// #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub(crate) enum DeviceIdentifier { Field(usize), @@ -303,11 +312,11 @@ async fn sp_info_mgs( let mut sp_list = all_sp_list .iter() .filter_map(|ignition| { - if matches!(ignition.details, SpIgnition::Yes { .. }) { - if ignition.id.type_ == SpType::Sled { - if args.matches_sp(&ignition.id) { - return Some(ignition.id); - } + if matches!(ignition.details, SpIgnition::Yes { .. }) + && ignition.id.type_ == SpType::Sled + { + if args.matches_sp(&ignition.id) { + return Some(ignition.id); } } None @@ -329,12 +338,8 @@ async fn sp_info_mgs( let mut handles = vec![]; for sp_id in sp_list { - let mgs_client = mgs_client.clone(); - let type_ = sp_id.type_; - let slot = sp_id.slot; - let handle = - tokio::spawn(async move { sp_info(mgs_client, type_, slot).await }); + tokio::spawn(sp_info(mgs_client.clone(), sp_id.type_, sp_id.slot)); handles.push((sp_id, handle)); } @@ -543,12 +548,12 @@ fn sp_info_csv( pub(crate) async fn sensor_metadata( input: &mut SensorInput, args: &SensorsArgs, -) -> Result<(SensorMetadata, SensorValues), anyhow::Error> { +) -> Result<(Arc, SensorValues), anyhow::Error> { let by_kind = if let Some(types) = &args.types { let mut h = HashSet::new(); for t in types { - h.insert(match Sensor::from_string("", &t) { + h.insert(match Sensor::from_string("", t) { None => bail!("invalid sensor kind {t}"), Some(s) => s.kind, }); @@ -559,11 +564,10 @@ pub(crate) async fn sensor_metadata( None }; - let by_name = if let Some(named) = &args.named { - Some(named.into_iter().collect::>()) - } else { - None - }; + let by_name = args + .named + .as_ref() + .map(|named| named.into_iter().collect::>()); let info = match input { SensorInput::MgsClient(ref mgs_client) => { @@ -634,7 +638,7 @@ pub(crate) async fn sensor_metadata( } Ok(( - SensorMetadata { + Arc::new(SensorMetadata { sensors_by_sensor, sensors_by_sensor_and_sp, sensors_by_id, @@ -643,12 +647,9 @@ pub(crate) async fn sensor_metadata( start_time: args.start, end_time: match args.end { Some(end) => Some(end), - None => match args.duration { - Some(duration) => Some(time + duration), - None => None, - }, + None => args.duration.map(|duration| time + duration), }, - }, + }), SensorValues { values, time, latencies: info.latencies }, )) } @@ -687,12 +688,12 @@ async fn sp_read_sensors( } } - Ok((rval, std::time::Instant::now().duration_since(start))) + Ok((rval, start.elapsed())) } async fn sp_data_mgs( mgs_client: &gateway_client::Client, - metadata: &'static SensorMetadata, + metadata: &Arc, ) -> Result { let mut values = HashMap::new(); let mut latencies = HashMap::new(); @@ -701,9 +702,10 @@ async fn sp_data_mgs( for sp_id in metadata.sensors_by_sp.keys() { let mgs_client = mgs_client.clone(); let id = *sp_id; + let metadata = Arc::clone(&metadata); let handle = tokio::spawn(async move { - sp_read_sensors(&mgs_client, &id, metadata).await + sp_read_sensors(&mgs_client, &id, &metadata).await }); handles.push((id, handle)); @@ -729,7 +731,7 @@ async fn sp_data_mgs( fn sp_data_csv( reader: &mut csv::Reader, position: &mut csv::Position, - metadata: &'static SensorMetadata, + metadata: &SensorMetadata, ) -> Result { let headers = reader.headers()?; let hlen = headers.len(); @@ -795,14 +797,14 @@ fn sp_data_csv( pub(crate) async fn sensor_data( input: &mut SensorInput, - metadata: &'static SensorMetadata, + metadata: &Arc, ) -> Result { match input { SensorInput::MgsClient(ref mgs_client) => { sp_data_mgs(mgs_client, metadata).await } SensorInput::CsvReader(reader, position) => { - sp_data_csv(reader, position, metadata) + sp_data_csv(reader, position, &metadata) } } } @@ -817,7 +819,8 @@ pub(crate) async fn cmd_mgs_sensors( args: &SensorsArgs, ) -> Result<(), anyhow::Error> { let mut input = if let Some(ref input) = args.input { - let file = File::open(input)?; + let file = File::open(input) + .with_context(|| format!("failed to open {input}"))?; SensorInput::CsvReader( csv::Reader::from_reader(file), csv::Position::new(), @@ -828,13 +831,6 @@ pub(crate) async fn cmd_mgs_sensors( let (metadata, mut values) = sensor_metadata(&mut input, args).await?; - // - // A bit of shenangians to force metadata to be 'static -- which allows - // us to share it with tasks. - // - let metadata = Box::leak(Box::new(metadata)); - let metadata: &_ = metadata; - let mut sensors = metadata.sensors_by_sensor.keys().collect::>(); sensors.sort(); @@ -939,7 +935,7 @@ pub(crate) async fn cmd_mgs_sensors( wakeup += tokio::time::Duration::from_millis(1000); } - values = sensor_data(&mut input, metadata).await?; + values = sensor_data(&mut input, &metadata).await?; if args.input.is_some() && values.time == 0 { break;