From bcd424fe284984b68f42b4f3c74a614fe0a27115 Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Wed, 12 Jun 2024 16:39:04 -0500 Subject: [PATCH 01/11] First commit for the support tool. Runs sanity checks and gathers data, compresses it and can read the header back for validation. --- src/build_dpkg.sh | 2 +- src/build_rust.sh | 2 +- src/rust/Cargo.lock | 56 ++++- src/rust/Cargo.toml | 1 + src/rust/lqos_support_tool/Cargo.toml | 16 ++ src/rust/lqos_support_tool/src/console.rs | 13 ++ src/rust/lqos_support_tool/src/lib.rs | 8 + src/rust/lqos_support_tool/src/main.rs | 70 ++++++ .../lqos_support_tool/src/sanity_checks.rs | 219 ++++++++++++++++++ .../lqos_support_tool/src/support_info.rs | 115 +++++++++ .../src/support_info/ip_link.rs | 38 +++ .../src/support_info/lqos_config.rs | 38 +++ .../src/support_info/service_config.rs | 50 ++++ .../support_info/systemctl_service_single.rs | 43 ++++ .../src/support_info/systemctl_services.rs | 39 ++++ .../src/support_info/task_journal.rs | 43 ++++ 16 files changed, 740 insertions(+), 13 deletions(-) create mode 100644 src/rust/lqos_support_tool/Cargo.toml create mode 100644 src/rust/lqos_support_tool/src/console.rs create mode 100644 src/rust/lqos_support_tool/src/lib.rs create mode 100644 src/rust/lqos_support_tool/src/main.rs create mode 100644 src/rust/lqos_support_tool/src/sanity_checks.rs create mode 100644 src/rust/lqos_support_tool/src/support_info.rs create mode 100644 src/rust/lqos_support_tool/src/support_info/ip_link.rs create mode 100644 src/rust/lqos_support_tool/src/support_info/lqos_config.rs create mode 100644 src/rust/lqos_support_tool/src/support_info/service_config.rs create mode 100644 src/rust/lqos_support_tool/src/support_info/systemctl_service_single.rs create mode 100644 src/rust/lqos_support_tool/src/support_info/systemctl_services.rs create mode 100644 src/rust/lqos_support_tool/src/support_info/task_journal.rs diff --git a/src/build_dpkg.sh b/src/build_dpkg.sh index 598d31e7..5c441d92 100755 --- a/src/build_dpkg.sh +++ b/src/build_dpkg.sh @@ -22,7 +22,7 @@ ETC_DIR=$DPKG_DIR/etc MOTD_DIR=$DPKG_DIR/etc/update-motd.d LQOS_FILES="graphInfluxDB.py influxDBdashboardTemplate.json integrationCommon.py integrationRestHttp.py integrationSplynx.py integrationUISP.py integrationSonar.py ispConfig.example.py LibreQoS.py lqos.example lqTools.py mikrotikFindIPv6.py network.example.json pythonCheck.py README.md scheduler.py ShapedDevices.example.csv" LQOS_BIN_FILES="lqos_scheduler.service.example lqosd.service.example lqos_node_manager.service.example" -RUSTPROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqos_node_manager lqusers lqos_setup lqos_map_perf" +RUSTPROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqos_node_manager lqusers lqos_setup lqos_map_perf lqos_support_tool" #################################################### # Clean any previous dist build diff --git a/src/build_rust.sh b/src/build_rust.sh index 253d13da..89007474 100755 --- a/src/build_rust.sh +++ b/src/build_rust.sh @@ -52,7 +52,7 @@ rustup update # Start building echo "Please wait while the system is compiled. Service will not be interrupted during this stage." -PROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqos_node_manager lqusers lqos_map_perf" +PROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqos_node_manager lqusers lqos_map_perf lqos_support_tool" mkdir -p bin/static pushd rust > /dev/null #cargo clean diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index ff210f15..343426f0 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -131,9 +131,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.83" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "async-compression" @@ -425,6 +425,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chacha20" version = "0.9.1" @@ -501,9 +507,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.4" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", "clap_derive", @@ -511,9 +517,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ "anstream", "anstyle", @@ -523,9 +529,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1565,9 +1571,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.154" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" @@ -1770,6 +1776,22 @@ dependencies = [ "uuid", ] +[[package]] +name = "lqos_support_tool" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "colored", + "libc", + "log", + "lqos_config", + "miniz_oxide", + "nix 0.29.0", + "serde", + "serde_cbor", +] + [[package]] name = "lqos_sys" version = "0.1.0" @@ -2075,7 +2097,19 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ "bitflags 2.5.0", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.1.1", + "libc", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "cfg_aliases 0.2.1", "libc", ] diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml index 6f139b48..506d744c 100644 --- a/src/rust/Cargo.toml +++ b/src/rust/Cargo.toml @@ -31,4 +31,5 @@ members = [ "lts_client", # Shared data and client-side code for long-term stats "lqos_map_perf", # A CLI tool for testing eBPF map performance "uisp", # REST support for the UISP API + "lqos_support_tool", # A Helper tool to make it easier to request/receive support ] diff --git a/src/rust/lqos_support_tool/Cargo.toml b/src/rust/lqos_support_tool/Cargo.toml new file mode 100644 index 00000000..f6335ead --- /dev/null +++ b/src/rust/lqos_support_tool/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "lqos_support_tool" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.86" +clap = { version = "4.5.7", features = ["derive"] } +colored = "2.1.0" +lqos_config = { path = "../lqos_config" } +serde = { version = "1.0", features = ["derive"] } +serde_cbor = "0" # For RFC8949/7409 format C binary objects +miniz_oxide = "0.7.1" +log = "0.4.21" # For compression +libc = "0.2.155" +nix = "0.29.0" diff --git a/src/rust/lqos_support_tool/src/console.rs b/src/rust/lqos_support_tool/src/console.rs new file mode 100644 index 00000000..f304be5a --- /dev/null +++ b/src/rust/lqos_support_tool/src/console.rs @@ -0,0 +1,13 @@ +use colored::Colorize; + +pub fn success(s: &str) { + println!("{} - {s}", "OK".bright_green()); +} + +pub fn warn(s: &str) { + println!("{} - {s}", "WARN".bright_yellow()); +} + +pub fn error(s: &str) { + println!("{} - {s}", "ERROR".bright_red()); +} \ No newline at end of file diff --git a/src/rust/lqos_support_tool/src/lib.rs b/src/rust/lqos_support_tool/src/lib.rs new file mode 100644 index 00000000..39d45773 --- /dev/null +++ b/src/rust/lqos_support_tool/src/lib.rs @@ -0,0 +1,8 @@ +//! Provides a support library for the support tool system. +mod support_info; +pub mod console; +mod sanity_checks; + +pub use support_info::gather_all_support_info; +pub use support_info::SupportDump; +pub use sanity_checks::run_sanity_checks; \ No newline at end of file diff --git a/src/rust/lqos_support_tool/src/main.rs b/src/rust/lqos_support_tool/src/main.rs new file mode 100644 index 00000000..5bf1e087 --- /dev/null +++ b/src/rust/lqos_support_tool/src/main.rs @@ -0,0 +1,70 @@ +//! Provides the start of a text-mode support tool for LibreQoS. It will double as +//! a library (see `lib.rs`) to provide similar functionality from the GUI. + +use std::path::Path; +use std::time::{SystemTime, UNIX_EPOCH}; +use clap::{Parser, Subcommand}; +use lqos_support_tool::{gather_all_support_info, run_sanity_checks, SupportDump}; + +#[derive(Parser)] +#[command(version = "1.0", about = "LibreQoS Support Tool", long_about = None, arg_required_else_help = true)] +struct Cli { + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Commands { + /// Sanity Checks your Configuration against your hardware + Sanity, + /// Gather Support Info and Save it to /tmp + Gather, + /// Summarize the contents of a support dump + Summarize { + /// The filename to read + filename: String + }, +} + +fn gather_dump() { + let dump = gather_all_support_info().unwrap(); + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + let filename = format!("/tmp/libreqos_{}.support", timestamp); + let path = Path::new(&filename); + std::fs::write(&path, dump.serialize_and_compress().unwrap()).unwrap(); + lqos_support_tool::console::success(&format!("Dump written to {}", filename)); +} + +fn summarize(filename: &str) { + let path = Path::new(filename); + if !path.exists() { + println!("Dump not found at {filename}"); + } else { + let bytes = std::fs::read(&path).unwrap(); + if let Ok(decoded) = SupportDump::from_bytes(&bytes) { + println!("{:40} {:10} : {:40?}", "ENTRY", "SIZE", "FILENAME"); + for entry in decoded.entries.iter() { + println!("{:40} {:10} : {:40?}", entry.name, entry.contents.len(), entry.filename); + } + } else { + println!("Dump did not decode"); + } + } +} + +fn sanity_checks() { + if let Err(e) = run_sanity_checks() { + println!("Sanity Check Failed: {e:?}"); + } +} + +fn main() { + let cli = Cli::parse(); + + match cli.command { + Some(Commands::Sanity) => sanity_checks(), + Some(Commands::Gather) => gather_dump(), + Some(Commands::Summarize { filename }) => summarize(&filename), + _ => {} + } +} \ No newline at end of file diff --git a/src/rust/lqos_support_tool/src/sanity_checks.rs b/src/rust/lqos_support_tool/src/sanity_checks.rs new file mode 100644 index 00000000..05719c87 --- /dev/null +++ b/src/rust/lqos_support_tool/src/sanity_checks.rs @@ -0,0 +1,219 @@ +use std::ffi::CString; +use std::path::Path; + +use anyhow::Error; +use log::error; +use serde::{Deserialize, Serialize}; + +use lqos_config::load_config; + +use crate::console::{error, success}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct SanityChecks { + results: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct SanityCheck { + pub name: String, + pub success: bool, + pub comments: String, +} + +pub fn run_sanity_checks() -> anyhow::Result { + println!("Running Sanity Checks"); + let mut results = Vec::new(); + + // Run the checks + config_exists(&mut results); + can_load_config(&mut results); + interfaces_exist(&mut results); + sanity_check_queues(&mut results); + + // Did any fail? + let mut any_errors = false; + for s in results.iter() { + if s.success { + success(&s.name); + } else { + error(&format!("{}: {}", s.name, s.comments)); + any_errors = true; + } + } + + if any_errors { + error("ERRORS FOUND DURING SANITY CHECK"); + } + + Ok(SanityChecks { results }) +} + +fn config_exists(results: &mut Vec) { + let path = Path::new("/etc/lqos.conf"); + let mut result = SanityCheck { + name: "Config File Exists".to_string(), + ..Default::default() + }; + if path.exists() { + result.success = true; + } else { + result.success = false; + result.comments = "/etc/lqos.conf could not be opened".to_string(); + } + + results.push(result); +} + +fn can_load_config(results: &mut Vec) { + let mut result = SanityCheck { + name: "Config File Can Be Loaded".to_string(), + ..Default::default() + }; + let cfg = load_config(); + if cfg.is_ok() { + result.success = true; + } else { + result.success = false; + result.comments = "Configuration file could not be loaded".to_string(); + } + results.push(result); +} + +pub fn interface_name_to_index(interface_name: &str) -> anyhow::Result { + use nix::libc::if_nametoindex; + let if_name = CString::new(interface_name)?; + let index = unsafe { if_nametoindex(if_name.as_ptr()) }; + if index == 0 { + Err(Error::msg(format!("Unknown interface: {interface_name}"))) + } else { + Ok(index) + } +} + +fn interfaces_exist(results: &mut Vec) { + if let Ok(cfg) = load_config() { + if cfg.on_a_stick_mode() { + if interface_name_to_index(&cfg.internet_interface()).is_ok() { + results.push(SanityCheck { + name: "Single Interface Exists".to_string(), + success: true, + comments: "".to_string(), + }); + } else { + results.push(SanityCheck { + name: "Single Interface Exists".to_string(), + success: false, + comments: format!("Interface {} is listed in /etc/lqos.conf - but that interface does not appear to exist in the Linux interface map", cfg.internet_interface()), + }); + } + } else { + if interface_name_to_index(&cfg.internet_interface()).is_ok() { + results.push(SanityCheck { + name: "Internet Interface Exists".to_string(), + success: true, + comments: "".to_string(), + }); + } else { + results.push(SanityCheck { + name: "Internet Interface Exists".to_string(), + success: false, + comments: format!("Interface {} is listed in /etc/lqos.conf - but that interface does not appear to exist in the Linux interface map", cfg.internet_interface()), + }); + } + + if interface_name_to_index(&cfg.isp_interface()).is_ok() { + results.push(SanityCheck { + name: "ISP Facing Interface Exists".to_string(), + success: true, + comments: "".to_string(), + }); + } else { + results.push(SanityCheck { + name: "ISP Facing Interface Exists".to_string(), + success: false, + comments: format!("Interface {} is listed in /etc/lqos.conf - but that interface does not appear to exist in the Linux interface map", cfg.isp_interface()), + }); + } + } + } +} + +fn check_queues(interface: &str) -> (i32, i32) { + let path = format!("/sys/class/net/{interface}/queues/"); + let sys_path = Path::new(&path); + if !sys_path.exists() { + return (0,0); + } + + let mut counts = (0, 0); + let paths = std::fs::read_dir(sys_path).unwrap(); + for path in paths { + if let Ok(path) = &path { + if path.path().is_dir() { + if let Some(filename) = path.path().file_name() { + if let Some(filename) = filename.to_str() { + if filename.starts_with("rx-") { + counts.0 += 1; + } else if filename.starts_with("tx-") { + counts.1 += 1; + } + } + } + } + } + } + + counts +} + +fn sanity_check_queues(results: &mut Vec) { + if let Ok(cfg) = load_config() { + if cfg.on_a_stick_mode() { + let counts = check_queues(&cfg.internet_interface()); + if counts.0 > 1 && counts.1 > 1 { + results.push(SanityCheck{ + name: "Queue Check (Internet Interface)".to_string(), + success: true, + comments: "".to_string(), + }); + } else { + results.push(SanityCheck{ + name: "Queue Check (Internet Interface)".to_string(), + success: false, + comments: format!("{} does not provide multiple RX and TX queues", cfg.internet_interface()), + }); + } + } else { + let counts = check_queues(&cfg.internet_interface()); + if counts.0 > 1 && counts.1 > 1 { + results.push(SanityCheck{ + name: "Queue Check (Internet Interface)".to_string(), + success: true, + comments: "".to_string(), + }); + } else { + results.push(SanityCheck{ + name: "Queue Check (Internet Interface)".to_string(), + success: false, + comments: format!("{} does not provide multiple RX and TX queues", cfg.internet_interface()), + }); + } + + let counts = check_queues(&cfg.isp_interface()); + if counts.0 > 1 && counts.1 > 1 { + results.push(SanityCheck{ + name: "Queue Check (ISP Facing Interface)".to_string(), + success: true, + comments: "".to_string(), + }); + } else { + results.push(SanityCheck{ + name: "Queue Check (ISP Facing Interface)".to_string(), + success: false, + comments: format!("{} does not provide multiple RX and TX queues", cfg.isp_interface()), + }); + } + } + } +} \ No newline at end of file diff --git a/src/rust/lqos_support_tool/src/support_info.rs b/src/rust/lqos_support_tool/src/support_info.rs new file mode 100644 index 00000000..1cb104df --- /dev/null +++ b/src/rust/lqos_support_tool/src/support_info.rs @@ -0,0 +1,115 @@ +use colored::Colorize; +use serde::{Deserialize, Serialize}; +use crate::console::error; +use crate::sanity_checks::{run_sanity_checks, SanityChecks}; + +mod ip_link; +mod systemctl_services; +mod systemctl_service_single; +mod lqos_config; +mod task_journal; +mod service_config; + +pub trait SupportInfo { + fn get_string(&self) -> String; + fn get_name(&self) -> String; + fn get_filename(&self) -> Option; + fn gather(&mut self) -> anyhow::Result<()>; +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SupportDump { + pub sanity_checks: SanityChecks, + pub entries: Vec +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DumpEntry { + pub name: String, + pub filename: Option, + pub contents: String, +} + +impl SupportDump { + pub fn serialize_and_compress(&self) -> anyhow::Result> { + let cbor_bytes = serde_cbor::to_vec(self)?; + let compressed_bytes = miniz_oxide::deflate::compress_to_vec(&cbor_bytes, 10); + Ok(compressed_bytes) + } + + pub fn from_bytes(raw_bytes: &[u8]) -> anyhow::Result { + let decompressed_bytes = miniz_oxide::inflate::decompress_to_vec(raw_bytes).unwrap(); + let deserialized = serde_cbor::from_slice(&decompressed_bytes)?; + Ok(deserialized) + } +} + +pub fn gather_all_support_info() -> anyhow::Result { + let sanity_checks = run_sanity_checks()?; + + let mut data_targets: Vec> = vec![ + lqos_config::LqosConfig::boxed(), + ip_link::IpLink::boxed(), + systemctl_services::SystemCtlServices::boxed(), + systemctl_service_single::SystemCtlService::boxed("lqosd"), + systemctl_service_single::SystemCtlService::boxed("lqos_node_manager"), + systemctl_service_single::SystemCtlService::boxed("lqos_scheduler"), + task_journal::TaskJournal::boxed("lqosd"), + task_journal::TaskJournal::boxed("lqos_node_manager"), + task_journal::TaskJournal::boxed("lqos_scheduler"), + service_config::ServiceConfig::boxed("ShapedDevices.csv"), + service_config::ServiceConfig::boxed("network.json"), + ]; + + for target in data_targets.iter_mut() { + println!("{} : {}", + "TASK-GATHER".cyan(), + target.get_name().yellow() + ); + if let Err(e) = target.gather() { + error(&e.to_string()); + } + } + + let mut dump = SupportDump { + sanity_checks, + entries: Vec::new(), + }; + + for target in data_targets.iter() { + let entry = DumpEntry { + name: target.get_name(), + filename: target.get_filename(), + contents: target.get_string(), + }; + dump.entries.push(entry); + } + //println!("{dump:#?}"); + + Ok(dump) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn round_trip() { + let original = SupportDump { + entries: vec![ + DumpEntry { + name: "Test".to_string(), + filename: None, + contents: "BLAH".to_string(), + } + ] + }; + let bytes = original.serialize_and_compress().unwrap(); + let restored = SupportDump::from_bytes(&bytes).unwrap(); + assert_eq!(original.entries.len(), restored.entries.len()); + assert_eq!(original.entries.len(), 1); + assert_eq!(original.entries[0].name, "Test"); + assert!(original.entries[0].filename.is_none()); + assert_eq!(original.entries[0].contents, "BLAH"); + } +} \ No newline at end of file diff --git a/src/rust/lqos_support_tool/src/support_info/ip_link.rs b/src/rust/lqos_support_tool/src/support_info/ip_link.rs new file mode 100644 index 00000000..589b1d96 --- /dev/null +++ b/src/rust/lqos_support_tool/src/support_info/ip_link.rs @@ -0,0 +1,38 @@ +use std::process::Command; +use crate::console::success; +use crate::support_info::SupportInfo; + +#[derive(Debug, Default)] +pub struct IpLink { + output: String, +} + +impl SupportInfo for IpLink { + fn get_string(&self) -> String { + self.output.to_string() + } + + fn get_name(&self) -> String { + "IP Link Information".to_string() + } + + fn get_filename(&self) -> Option { + None + } + + fn gather(&mut self) -> anyhow::Result<()> { + let output = Command::new("/sbin/ip") + .arg("link") + .output()?; + let out_str = String::from_utf8_lossy(output.stdout.as_slice()); + self.output = out_str.to_string(); + success("Gathered `ip link` data"); + Ok(()) + } +} + +impl IpLink { + pub fn boxed() -> Box { + Box::new(Self::default()) + } +} \ No newline at end of file diff --git a/src/rust/lqos_support_tool/src/support_info/lqos_config.rs b/src/rust/lqos_support_tool/src/support_info/lqos_config.rs new file mode 100644 index 00000000..f605cf8b --- /dev/null +++ b/src/rust/lqos_support_tool/src/support_info/lqos_config.rs @@ -0,0 +1,38 @@ +use std::path::Path; +use crate::console::success; +use crate::support_info::SupportInfo; + +#[derive(Default)] +pub struct LqosConfig { + output: String, +} + +impl SupportInfo for LqosConfig { + fn get_string(&self) -> String { + self.output.to_string() + } + + fn get_name(&self) -> String { + "LibreQoS Config File".to_string() + } + + fn get_filename(&self) -> Option { + Some("/etc/lqos.conf".to_string()) + } + + fn gather(&mut self) -> anyhow::Result<()> { + let path = Path::new("/etc/lqos.conf"); + if !path.exists() { + anyhow::bail!("/etc/lqos.conf could not be opened"); + } + self.output = std::fs::read_to_string(path)?; + success("Gathered /etc/lqos.conf"); + Ok(()) + } +} + +impl LqosConfig { + pub fn boxed() -> Box { + Box::new(Self::default()) + } +} \ No newline at end of file diff --git a/src/rust/lqos_support_tool/src/support_info/service_config.rs b/src/rust/lqos_support_tool/src/support_info/service_config.rs new file mode 100644 index 00000000..6c087c4b --- /dev/null +++ b/src/rust/lqos_support_tool/src/support_info/service_config.rs @@ -0,0 +1,50 @@ +use std::path::Path; +use lqos_config::load_config; +use crate::console::{error, success}; +use crate::support_info::SupportInfo; + +#[derive(Default)] +pub struct ServiceConfig { + target: String, + output: String, +} + +impl SupportInfo for ServiceConfig { + fn get_string(&self) -> String { + self.output.to_string() + } + + fn get_name(&self) -> String { + format!("Config File: {}", self.target) + } + + fn get_filename(&self) -> Option { + let cfg = load_config(); + if let Ok(cfg) = cfg { + Some(format!("{}{}", cfg.lqos_directory, self.target)) + } else { + error("Unable to read configuration!"); + None + } + } + + fn gather(&mut self) -> anyhow::Result<()> { + let cfg = load_config()?; + let path = Path::new(&cfg.lqos_directory).join(&self.target); + if !path.exists() { + anyhow::bail!("Could not read from {:?}", path); + } + self.output = std::fs::read_to_string(path)?; + success(&format!("Gathered {}", self.target)); + Ok(()) + } +} + +impl ServiceConfig { + pub fn boxed(target: S) -> Box { + Box::new(Self { + target: target.to_string(), + ..Default::default() + }) + } +} \ No newline at end of file diff --git a/src/rust/lqos_support_tool/src/support_info/systemctl_service_single.rs b/src/rust/lqos_support_tool/src/support_info/systemctl_service_single.rs new file mode 100644 index 00000000..4ef925a3 --- /dev/null +++ b/src/rust/lqos_support_tool/src/support_info/systemctl_service_single.rs @@ -0,0 +1,43 @@ +use std::process::Command; +use crate::console::success; +use crate::support_info::SupportInfo; + +#[derive(Default)] +pub struct SystemCtlService { + target: String, + output: String, +} + +impl SupportInfo for SystemCtlService { + fn get_string(&self) -> String { + self.output.to_string() + } + + fn get_name(&self) -> String { + format!("SystemCtl Status ({})", self.target) + } + + fn get_filename(&self) -> Option { + None + } + + fn gather(&mut self) -> anyhow::Result<()> { + let out = Command::new("/bin/systemctl") + .args(&["--no-pager", "status", &self.target]) + .output()?; + + self.output = String::from_utf8_lossy(&out.stdout).to_string(); + success(&format!("Gathered systemctl status for {}", self.target)); + + Ok(()) + } +} + +impl SystemCtlService { + pub fn boxed(target: S) -> Box { + Box::new(Self { + target: target.to_string(), + ..Default::default() + }) + } +} \ No newline at end of file diff --git a/src/rust/lqos_support_tool/src/support_info/systemctl_services.rs b/src/rust/lqos_support_tool/src/support_info/systemctl_services.rs new file mode 100644 index 00000000..21ef5c39 --- /dev/null +++ b/src/rust/lqos_support_tool/src/support_info/systemctl_services.rs @@ -0,0 +1,39 @@ +use std::process::Command; +use crate::console::success; +use crate::support_info::SupportInfo; + +#[derive(Default)] +pub struct SystemCtlServices { + output: String, +} + +impl SupportInfo for SystemCtlServices { + fn get_string(&self) -> String { + self.output.to_string() + } + + fn get_name(&self) -> String { + "SystemCtl Status".to_string() + } + + fn get_filename(&self) -> Option { + None + } + + fn gather(&mut self) -> anyhow::Result<()> { + let out = Command::new("/bin/systemctl") + .args(&["--no-pager", "status"]) + .output()?; + + self.output = String::from_utf8_lossy(&out.stdout).to_string(); + success("Gathered global `systemctl status`"); + + Ok(()) + } +} + +impl SystemCtlServices { + pub fn boxed() -> Box { + Box::new(Self::default()) + } +} \ No newline at end of file diff --git a/src/rust/lqos_support_tool/src/support_info/task_journal.rs b/src/rust/lqos_support_tool/src/support_info/task_journal.rs new file mode 100644 index 00000000..61b0e1da --- /dev/null +++ b/src/rust/lqos_support_tool/src/support_info/task_journal.rs @@ -0,0 +1,43 @@ +use std::process::Command; +use crate::console::success; +use crate::support_info::SupportInfo; + +#[derive(Default)] +pub struct TaskJournal { + target: String, + output: String, +} + +impl SupportInfo for TaskJournal { + fn get_string(&self) -> String { + self.output.to_string() + } + + fn get_name(&self) -> String { + format!("Journal ({})", self.target) + } + + fn get_filename(&self) -> Option { + None + } + + fn gather(&mut self) -> anyhow::Result<()> { + let out = Command::new("/bin/journalctl") + .args(&["--no-pager", "-u", &self.target]) + .output()?; + + self.output = String::from_utf8_lossy(&out.stdout).to_string(); + success(&format!("Gathered journalctl status for {}", self.target)); + + Ok(()) + } +} + +impl TaskJournal { + pub fn boxed(target: S) -> Box { + Box::new(Self { + target: target.to_string(), + ..Default::default() + }) + } +} \ No newline at end of file From cbec81ab39b68fef8037f446b95abf3e89d39c71 Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Thu, 13 Jun 2024 08:14:35 -0500 Subject: [PATCH 02/11] Refactor sanity checks for cleaner layout --- .../lqos_support_tool/src/sanity_checks.rs | 187 +----------------- .../src/sanity_checks/config_sane.rs | 34 ++++ .../src/sanity_checks/interfaces.rs | 63 ++++++ .../src/sanity_checks/queues.rs | 82 ++++++++ 4 files changed, 186 insertions(+), 180 deletions(-) create mode 100644 src/rust/lqos_support_tool/src/sanity_checks/config_sane.rs create mode 100644 src/rust/lqos_support_tool/src/sanity_checks/interfaces.rs create mode 100644 src/rust/lqos_support_tool/src/sanity_checks/queues.rs diff --git a/src/rust/lqos_support_tool/src/sanity_checks.rs b/src/rust/lqos_support_tool/src/sanity_checks.rs index 05719c87..2e61d6e4 100644 --- a/src/rust/lqos_support_tool/src/sanity_checks.rs +++ b/src/rust/lqos_support_tool/src/sanity_checks.rs @@ -1,12 +1,8 @@ -use std::ffi::CString; -use std::path::Path; +mod config_sane; +mod interfaces; +mod queues; -use anyhow::Error; -use log::error; use serde::{Deserialize, Serialize}; - -use lqos_config::load_config; - use crate::console::{error, success}; #[derive(Debug, Serialize, Deserialize)] @@ -26,10 +22,10 @@ pub fn run_sanity_checks() -> anyhow::Result { let mut results = Vec::new(); // Run the checks - config_exists(&mut results); - can_load_config(&mut results); - interfaces_exist(&mut results); - sanity_check_queues(&mut results); + config_sane::config_exists(&mut results); + config_sane::can_load_config(&mut results); + interfaces::interfaces_exist(&mut results); + queues::sanity_check_queues(&mut results); // Did any fail? let mut any_errors = false; @@ -47,173 +43,4 @@ pub fn run_sanity_checks() -> anyhow::Result { } Ok(SanityChecks { results }) -} - -fn config_exists(results: &mut Vec) { - let path = Path::new("/etc/lqos.conf"); - let mut result = SanityCheck { - name: "Config File Exists".to_string(), - ..Default::default() - }; - if path.exists() { - result.success = true; - } else { - result.success = false; - result.comments = "/etc/lqos.conf could not be opened".to_string(); - } - - results.push(result); -} - -fn can_load_config(results: &mut Vec) { - let mut result = SanityCheck { - name: "Config File Can Be Loaded".to_string(), - ..Default::default() - }; - let cfg = load_config(); - if cfg.is_ok() { - result.success = true; - } else { - result.success = false; - result.comments = "Configuration file could not be loaded".to_string(); - } - results.push(result); -} - -pub fn interface_name_to_index(interface_name: &str) -> anyhow::Result { - use nix::libc::if_nametoindex; - let if_name = CString::new(interface_name)?; - let index = unsafe { if_nametoindex(if_name.as_ptr()) }; - if index == 0 { - Err(Error::msg(format!("Unknown interface: {interface_name}"))) - } else { - Ok(index) - } -} - -fn interfaces_exist(results: &mut Vec) { - if let Ok(cfg) = load_config() { - if cfg.on_a_stick_mode() { - if interface_name_to_index(&cfg.internet_interface()).is_ok() { - results.push(SanityCheck { - name: "Single Interface Exists".to_string(), - success: true, - comments: "".to_string(), - }); - } else { - results.push(SanityCheck { - name: "Single Interface Exists".to_string(), - success: false, - comments: format!("Interface {} is listed in /etc/lqos.conf - but that interface does not appear to exist in the Linux interface map", cfg.internet_interface()), - }); - } - } else { - if interface_name_to_index(&cfg.internet_interface()).is_ok() { - results.push(SanityCheck { - name: "Internet Interface Exists".to_string(), - success: true, - comments: "".to_string(), - }); - } else { - results.push(SanityCheck { - name: "Internet Interface Exists".to_string(), - success: false, - comments: format!("Interface {} is listed in /etc/lqos.conf - but that interface does not appear to exist in the Linux interface map", cfg.internet_interface()), - }); - } - - if interface_name_to_index(&cfg.isp_interface()).is_ok() { - results.push(SanityCheck { - name: "ISP Facing Interface Exists".to_string(), - success: true, - comments: "".to_string(), - }); - } else { - results.push(SanityCheck { - name: "ISP Facing Interface Exists".to_string(), - success: false, - comments: format!("Interface {} is listed in /etc/lqos.conf - but that interface does not appear to exist in the Linux interface map", cfg.isp_interface()), - }); - } - } - } -} - -fn check_queues(interface: &str) -> (i32, i32) { - let path = format!("/sys/class/net/{interface}/queues/"); - let sys_path = Path::new(&path); - if !sys_path.exists() { - return (0,0); - } - - let mut counts = (0, 0); - let paths = std::fs::read_dir(sys_path).unwrap(); - for path in paths { - if let Ok(path) = &path { - if path.path().is_dir() { - if let Some(filename) = path.path().file_name() { - if let Some(filename) = filename.to_str() { - if filename.starts_with("rx-") { - counts.0 += 1; - } else if filename.starts_with("tx-") { - counts.1 += 1; - } - } - } - } - } - } - - counts -} - -fn sanity_check_queues(results: &mut Vec) { - if let Ok(cfg) = load_config() { - if cfg.on_a_stick_mode() { - let counts = check_queues(&cfg.internet_interface()); - if counts.0 > 1 && counts.1 > 1 { - results.push(SanityCheck{ - name: "Queue Check (Internet Interface)".to_string(), - success: true, - comments: "".to_string(), - }); - } else { - results.push(SanityCheck{ - name: "Queue Check (Internet Interface)".to_string(), - success: false, - comments: format!("{} does not provide multiple RX and TX queues", cfg.internet_interface()), - }); - } - } else { - let counts = check_queues(&cfg.internet_interface()); - if counts.0 > 1 && counts.1 > 1 { - results.push(SanityCheck{ - name: "Queue Check (Internet Interface)".to_string(), - success: true, - comments: "".to_string(), - }); - } else { - results.push(SanityCheck{ - name: "Queue Check (Internet Interface)".to_string(), - success: false, - comments: format!("{} does not provide multiple RX and TX queues", cfg.internet_interface()), - }); - } - - let counts = check_queues(&cfg.isp_interface()); - if counts.0 > 1 && counts.1 > 1 { - results.push(SanityCheck{ - name: "Queue Check (ISP Facing Interface)".to_string(), - success: true, - comments: "".to_string(), - }); - } else { - results.push(SanityCheck{ - name: "Queue Check (ISP Facing Interface)".to_string(), - success: false, - comments: format!("{} does not provide multiple RX and TX queues", cfg.isp_interface()), - }); - } - } - } } \ No newline at end of file diff --git a/src/rust/lqos_support_tool/src/sanity_checks/config_sane.rs b/src/rust/lqos_support_tool/src/sanity_checks/config_sane.rs new file mode 100644 index 00000000..60277877 --- /dev/null +++ b/src/rust/lqos_support_tool/src/sanity_checks/config_sane.rs @@ -0,0 +1,34 @@ +use std::path::Path; +use lqos_config::load_config; +use crate::sanity_checks::SanityCheck; + +pub fn config_exists(results: &mut Vec) { + let path = Path::new("/etc/lqos.conf"); + let mut result = SanityCheck { + name: "Config File Exists".to_string(), + ..Default::default() + }; + if path.exists() { + result.success = true; + } else { + result.success = false; + result.comments = "/etc/lqos.conf could not be opened".to_string(); + } + + results.push(result); +} + +pub fn can_load_config(results: &mut Vec) { + let mut result = SanityCheck { + name: "Config File Can Be Loaded".to_string(), + ..Default::default() + }; + let cfg = load_config(); + if cfg.is_ok() { + result.success = true; + } else { + result.success = false; + result.comments = "Configuration file could not be loaded".to_string(); + } + results.push(result); +} \ No newline at end of file diff --git a/src/rust/lqos_support_tool/src/sanity_checks/interfaces.rs b/src/rust/lqos_support_tool/src/sanity_checks/interfaces.rs new file mode 100644 index 00000000..99d872ba --- /dev/null +++ b/src/rust/lqos_support_tool/src/sanity_checks/interfaces.rs @@ -0,0 +1,63 @@ +use std::ffi::CString; +use anyhow::Error; +use lqos_config::load_config; +use crate::sanity_checks::SanityCheck; + +fn interface_name_to_index(interface_name: &str) -> anyhow::Result { + use nix::libc::if_nametoindex; + let if_name = CString::new(interface_name)?; + let index = unsafe { if_nametoindex(if_name.as_ptr()) }; + if index == 0 { + Err(Error::msg(format!("Unknown interface: {interface_name}"))) + } else { + Ok(index) + } +} + +pub fn interfaces_exist(results: &mut Vec) { + if let Ok(cfg) = load_config() { + if cfg.on_a_stick_mode() { + if interface_name_to_index(&cfg.internet_interface()).is_ok() { + results.push(SanityCheck { + name: "Single Interface Exists".to_string(), + success: true, + comments: "".to_string(), + }); + } else { + results.push(SanityCheck { + name: "Single Interface Exists".to_string(), + success: false, + comments: format!("Interface {} is listed in /etc/lqos.conf - but that interface does not appear to exist in the Linux interface map", cfg.internet_interface()), + }); + } + } else { + if interface_name_to_index(&cfg.internet_interface()).is_ok() { + results.push(SanityCheck { + name: "Internet Interface Exists".to_string(), + success: true, + comments: "".to_string(), + }); + } else { + results.push(SanityCheck { + name: "Internet Interface Exists".to_string(), + success: false, + comments: format!("Interface {} is listed in /etc/lqos.conf - but that interface does not appear to exist in the Linux interface map", cfg.internet_interface()), + }); + } + + if interface_name_to_index(&cfg.isp_interface()).is_ok() { + results.push(SanityCheck { + name: "ISP Facing Interface Exists".to_string(), + success: true, + comments: "".to_string(), + }); + } else { + results.push(SanityCheck { + name: "ISP Facing Interface Exists".to_string(), + success: false, + comments: format!("Interface {} is listed in /etc/lqos.conf - but that interface does not appear to exist in the Linux interface map", cfg.isp_interface()), + }); + } + } + } +} \ No newline at end of file diff --git a/src/rust/lqos_support_tool/src/sanity_checks/queues.rs b/src/rust/lqos_support_tool/src/sanity_checks/queues.rs new file mode 100644 index 00000000..8785628f --- /dev/null +++ b/src/rust/lqos_support_tool/src/sanity_checks/queues.rs @@ -0,0 +1,82 @@ +use std::path::Path; +use lqos_config::load_config; +use crate::sanity_checks::SanityCheck; + +fn check_queues(interface: &str) -> (i32, i32) { + let path = format!("/sys/class/net/{interface}/queues/"); + let sys_path = Path::new(&path); + if !sys_path.exists() { + return (0,0); + } + + let mut counts = (0, 0); + let paths = std::fs::read_dir(sys_path).unwrap(); + for path in paths { + if let Ok(path) = &path { + if path.path().is_dir() { + if let Some(filename) = path.path().file_name() { + if let Some(filename) = filename.to_str() { + if filename.starts_with("rx-") { + counts.0 += 1; + } else if filename.starts_with("tx-") { + counts.1 += 1; + } + } + } + } + } + } + + counts +} + +pub fn sanity_check_queues(results: &mut Vec) { + if let Ok(cfg) = load_config() { + if cfg.on_a_stick_mode() { + let counts = check_queues(&cfg.internet_interface()); + if counts.0 > 1 && counts.1 > 1 { + results.push(SanityCheck{ + name: "Queue Check (Internet Interface)".to_string(), + success: true, + comments: "".to_string(), + }); + } else { + results.push(SanityCheck{ + name: "Queue Check (Internet Interface)".to_string(), + success: false, + comments: format!("{} does not provide multiple RX and TX queues", cfg.internet_interface()), + }); + } + } else { + let counts = check_queues(&cfg.internet_interface()); + if counts.0 > 1 && counts.1 > 1 { + results.push(SanityCheck{ + name: "Queue Check (Internet Interface)".to_string(), + success: true, + comments: "".to_string(), + }); + } else { + results.push(SanityCheck{ + name: "Queue Check (Internet Interface)".to_string(), + success: false, + comments: format!("{} does not provide multiple RX and TX queues", cfg.internet_interface()), + }); + } + + let counts = check_queues(&cfg.isp_interface()); + if counts.0 > 1 && counts.1 > 1 { + results.push(SanityCheck{ + name: "Queue Check (ISP Facing Interface)".to_string(), + success: true, + comments: "".to_string(), + }); + } else { + results.push(SanityCheck{ + name: "Queue Check (ISP Facing Interface)".to_string(), + success: false, + comments: format!("{} does not provide multiple RX and TX queues", cfg.isp_interface()), + }); + } + } + } +} \ No newline at end of file From 33cae5799be9ae0dbdbc5ba3c145fdd420ad1568 Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Thu, 13 Jun 2024 08:41:18 -0500 Subject: [PATCH 03/11] Add bridge membership and interface status checks (interface status are informational) --- src/rust/Cargo.lock | 1 + src/rust/lqos_support_tool/Cargo.toml | 1 + .../lqos_support_tool/src/sanity_checks.rs | 3 + .../src/sanity_checks/bridge.rs | 124 ++++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 src/rust/lqos_support_tool/src/sanity_checks/bridge.rs diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index 343426f0..3e5a6cce 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -1790,6 +1790,7 @@ dependencies = [ "nix 0.29.0", "serde", "serde_cbor", + "serde_json", ] [[package]] diff --git a/src/rust/lqos_support_tool/Cargo.toml b/src/rust/lqos_support_tool/Cargo.toml index f6335ead..9d5f1080 100644 --- a/src/rust/lqos_support_tool/Cargo.toml +++ b/src/rust/lqos_support_tool/Cargo.toml @@ -14,3 +14,4 @@ miniz_oxide = "0.7.1" log = "0.4.21" # For compression libc = "0.2.155" nix = "0.29.0" +serde_json = "1.0.117" diff --git a/src/rust/lqos_support_tool/src/sanity_checks.rs b/src/rust/lqos_support_tool/src/sanity_checks.rs index 2e61d6e4..d1d5e23f 100644 --- a/src/rust/lqos_support_tool/src/sanity_checks.rs +++ b/src/rust/lqos_support_tool/src/sanity_checks.rs @@ -1,6 +1,7 @@ mod config_sane; mod interfaces; mod queues; +mod bridge; use serde::{Deserialize, Serialize}; use crate::console::{error, success}; @@ -26,6 +27,8 @@ pub fn run_sanity_checks() -> anyhow::Result { config_sane::can_load_config(&mut results); interfaces::interfaces_exist(&mut results); queues::sanity_check_queues(&mut results); + bridge::check_interface_status(&mut results); + bridge::check_bridge(&mut results); // Did any fail? let mut any_errors = false; diff --git a/src/rust/lqos_support_tool/src/sanity_checks/bridge.rs b/src/rust/lqos_support_tool/src/sanity_checks/bridge.rs new file mode 100644 index 00000000..37882788 --- /dev/null +++ b/src/rust/lqos_support_tool/src/sanity_checks/bridge.rs @@ -0,0 +1,124 @@ +use std::process::Command; +use lqos_config::load_config; +use crate::sanity_checks::SanityCheck; + +pub fn check_bridge(results: &mut Vec) { + if let Ok(cfg) = load_config() { + if let Ok(interfaces) = get_interfaces_from_ip_link() { + // On a stick mode is bridge-free + if cfg.on_a_stick_mode() { + results.push(SanityCheck{ + name: format!("Single Interface Mode: Bridges Ignored"), + success: true, + comments: "".to_string(), + }); + return; + } + + // Is the XDP bridge enabled? + if let Some(bridge) = &cfg.bridge { + if bridge.use_xdp_bridge { + for bridge_if in interfaces + .iter() + .filter(|bridge_if| bridge_if.link_type == "ether" && bridge_if.operational_state == "UP") + { + // We found a bridge. Check member interfaces to check that it does NOT include any XDP + // bridge members. + let in_bridge: Vec<&IpLinkInterface> = interfaces + .iter() + .filter(|member_if| { + if let Some(master) = &member_if.master { + master == &bridge_if.name + } else { + false + } + }) + .filter(|member_if| member_if.name == cfg.internet_interface() || member_if.name == cfg.isp_interface()) + .collect(); + + if in_bridge.len() == 2 { + results.push(SanityCheck{ + name: format!("Linux Bridge AND XDP Bridge At Once ({})", bridge_if.name), + success: false, + comments: format!("Bridge ({}) contains both the internet and ISP interfaces, and you have the xdp_bridge enabled. This is not supported.", bridge_if.name), + }); + } else { + results.push(SanityCheck{ + name: format!("Bridge Membership Check ({})", bridge_if.name), + success: true, + comments: "".to_string(), + }); + } + } + } + } + } + } +} + +pub fn check_interface_status(results: &mut Vec) { + if let Ok(cfg) = load_config() { + if let Ok(interfaces) = get_interfaces_from_ip_link() { + if let Some(stick) = &cfg.single_interface { + if let Some(iface) = interfaces.iter().find(|i| i.name == stick.interface) { + results.push(SanityCheck{ + name: format!("Interface {} in state {}", iface.name, iface.operational_state), + success: true, + comments: "".to_string(), + }); + } + } else if let Some(bridge) = &cfg.bridge { + if let Some(iface) = interfaces.iter().find(|i| i.name == bridge.to_internet) { + results.push(SanityCheck{ + name: format!("Interface {} in state {}", iface.name, iface.operational_state), + success: true, + comments: "".to_string(), + }); + } + if let Some(iface) = interfaces.iter().find(|i| i.name == bridge.to_network) { + results.push(SanityCheck{ + name: format!("Interface {} in state {}", iface.name, iface.operational_state), + success: true, + comments: "".to_string(), + }); + } + } + } + } +} + +#[derive(Debug)] +struct IpLinkInterface { + pub name: String, + pub index: u32, + pub operational_state: String, + pub link_type: String, + pub master: Option, +} + +fn get_interfaces_from_ip_link() -> anyhow::Result> { + let output = Command::new("/sbin/ip") + .args(["-j", "link"]) + .output()?; + let output = String::from_utf8(output.stdout)?; + let output_json = serde_json::from_str::(&output)?; + + let mut interfaces = Vec::new(); + for interface in output_json.as_array().unwrap() { + let name = interface["ifname"].as_str().unwrap().to_string(); + let index = interface["ifindex"].as_u64().unwrap() as u32; + let operstate = interface["operstate"].as_str().unwrap().to_string(); + let link_type = interface["link_type"].as_str().unwrap().to_string(); + let master = interface["master"].as_str().map(|s| s.to_string()); + + interfaces.push(IpLinkInterface { + name, + index, + operational_state: operstate, + link_type, + master, + }); + } + + Ok(interfaces) +} \ No newline at end of file From e986816081d69e8633a577e71d82a7f0cf49d70c Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Thu, 13 Jun 2024 09:27:13 -0500 Subject: [PATCH 04/11] Add Shaped Devices and Network.Json sanity checks --- src/rust/lqos_support_tool/src/main.rs | 6 ++ .../lqos_support_tool/src/sanity_checks.rs | 12 +++- .../src/sanity_checks/net_json.rs | 68 +++++++++++++++++++ .../src/sanity_checks/shaped_devices.rs | 66 ++++++++++++++++++ 4 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 src/rust/lqos_support_tool/src/sanity_checks/net_json.rs create mode 100644 src/rust/lqos_support_tool/src/sanity_checks/shaped_devices.rs diff --git a/src/rust/lqos_support_tool/src/main.rs b/src/rust/lqos_support_tool/src/main.rs index 5bf1e087..c4ce3ff6 100644 --- a/src/rust/lqos_support_tool/src/main.rs +++ b/src/rust/lqos_support_tool/src/main.rs @@ -42,6 +42,12 @@ fn summarize(filename: &str) { } else { let bytes = std::fs::read(&path).unwrap(); if let Ok(decoded) = SupportDump::from_bytes(&bytes) { + println!("{:50} {:10} {}", "Sanity Check", "Success?", "Comment"); + for entry in decoded.sanity_checks.results.iter() { + println!("{:50} {:10} {}", entry.name, entry.success, entry.comments); + } + println!(); + println!("{:40} {:10} : {:40?}", "ENTRY", "SIZE", "FILENAME"); for entry in decoded.entries.iter() { println!("{:40} {:10} : {:40?}", entry.name, entry.contents.len(), entry.filename); diff --git a/src/rust/lqos_support_tool/src/sanity_checks.rs b/src/rust/lqos_support_tool/src/sanity_checks.rs index d1d5e23f..d2d879c1 100644 --- a/src/rust/lqos_support_tool/src/sanity_checks.rs +++ b/src/rust/lqos_support_tool/src/sanity_checks.rs @@ -2,13 +2,15 @@ mod config_sane; mod interfaces; mod queues; mod bridge; +mod net_json; +mod shaped_devices; use serde::{Deserialize, Serialize}; use crate::console::{error, success}; #[derive(Debug, Serialize, Deserialize)] pub struct SanityChecks { - results: Vec, + pub results: Vec, } #[derive(Debug, Serialize, Deserialize, Default)] @@ -29,12 +31,18 @@ pub fn run_sanity_checks() -> anyhow::Result { queues::sanity_check_queues(&mut results); bridge::check_interface_status(&mut results); bridge::check_bridge(&mut results); + net_json::check_net_json_exists(&mut results); + net_json::can_we_load_net_json(&mut results); + net_json::can_we_parse_net_json(&mut results); + shaped_devices::shaped_devices_exists(&mut results); + shaped_devices::can_we_read_shaped_devices(&mut results); + shaped_devices::parent_check(&mut results); // Did any fail? let mut any_errors = false; for s in results.iter() { if s.success { - success(&s.name); + success(&format!("{} {}", s.name, s.comments)); } else { error(&format!("{}: {}", s.name, s.comments)); any_errors = true; diff --git a/src/rust/lqos_support_tool/src/sanity_checks/net_json.rs b/src/rust/lqos_support_tool/src/sanity_checks/net_json.rs new file mode 100644 index 00000000..50a70253 --- /dev/null +++ b/src/rust/lqos_support_tool/src/sanity_checks/net_json.rs @@ -0,0 +1,68 @@ +use std::path::Path; +use serde_json::Value; +use lqos_config::load_config; +use crate::sanity_checks::SanityCheck; + +pub fn check_net_json_exists(results: &mut Vec) { + if let Ok(cfg) = load_config() { + let path = Path::new(&cfg.lqos_directory).join("network.json"); + if path.exists() { + results.push(SanityCheck{ + name: "network.json exists".to_string(), + success: true, + comments: "".to_string(), + }); + } else { + results.push(SanityCheck{ + name: "network.json exists".to_string(), + success: false, + comments: format!("File not found at {:?}", path), + }); + } + } +} + +pub fn can_we_load_net_json(results: &mut Vec) { + if let Ok(cfg) = load_config() { + let path = Path::new(&cfg.lqos_directory).join("network.json"); + if path.exists() { + if let Ok(str) = std::fs::read_to_string(path) { + match serde_json::from_str::(&str) { + Ok(json) => { + results.push(SanityCheck{ + name: "network.json is parseable JSON".to_string(), + success: true, + comments: "".to_string(), + }); + } + Err(e) => { + results.push(SanityCheck{ + name: "network.json is parseable JSON".to_string(), + success: false, + comments: format!("{e:?}"), + }); + } + } + } + } + } +} + +pub fn can_we_parse_net_json(results: &mut Vec) { + match lqos_config::NetworkJson::load() { + Ok(json) => { + results.push(SanityCheck{ + name: "network.json is valid JSON".to_string(), + success: true, + comments: "".to_string(), + }); + } + Err(e) => { + results.push(SanityCheck{ + name: "network.json is valid JSON".to_string(), + success: false, + comments: format!("{e:?}"), + }); + } + } +} \ No newline at end of file diff --git a/src/rust/lqos_support_tool/src/sanity_checks/shaped_devices.rs b/src/rust/lqos_support_tool/src/sanity_checks/shaped_devices.rs new file mode 100644 index 00000000..edc387e9 --- /dev/null +++ b/src/rust/lqos_support_tool/src/sanity_checks/shaped_devices.rs @@ -0,0 +1,66 @@ +use std::path::Path; +use lqos_config::load_config; +use crate::sanity_checks::SanityCheck; + +pub fn shaped_devices_exists(results: &mut Vec) { + if let Ok(cfg) = load_config() { + let path = Path::new(&cfg.lqos_directory).join("ShapedDevices.csv"); + if path.exists() { + results.push(SanityCheck{ + name: "ShapedDevices.csv exists".to_string(), + success: true, + comments: "".to_string(), + }); + } else { + results.push(SanityCheck{ + name: "ShapedDevices.csv exists".to_string(), + success: false, + comments: format!("File not found at {:?}", path), + }); + } + } +} + +pub fn can_we_read_shaped_devices(results: &mut Vec) { + match lqos_config::ConfigShapedDevices::load() { + Ok(sd) => { + results.push(SanityCheck{ + name: "ShapedDevices.csv Loads?".to_string(), + success: true, + comments: format!("{} Devices Found", sd.devices.len()), + }); + } + Err(e) => { + results.push(SanityCheck{ + name: "ShapedDevices.csv Loads?".to_string(), + success: false, + comments: format!("{e:?}"), + }); + } + } +} + +pub fn parent_check(results: &mut Vec) { + if let Ok(net_json) = lqos_config::NetworkJson::load() { + if net_json.nodes.len() < 2 { + results.push(SanityCheck{ + name: "Flat Network - Skipping Parent Check".to_string(), + success: true, + comments: String::new(), + }); + return; + } + + if let Ok(shaped_devices) = lqos_config::ConfigShapedDevices::load() { + for sd in shaped_devices.devices.iter() { + if !net_json.nodes.iter().any(|n| n.name == sd.parent_node) { + results.push(SanityCheck{ + name: "Shaped Device Invalid Parent".to_string(), + success: false, + comments: format!("Device {}/{} is parented to {} - which does not exist", sd.device_name, sd.device_id, sd.parent_node), + }); + } + } + } + } +} \ No newline at end of file From 464d1f4a5ce7f1d3eedeb31453b736393a383844 Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Thu, 13 Jun 2024 10:05:11 -0500 Subject: [PATCH 05/11] Add 'expand' command to the CLI to extract dump files. --- src/rust/lqos_support_tool/src/main.rs | 47 ++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/rust/lqos_support_tool/src/main.rs b/src/rust/lqos_support_tool/src/main.rs index c4ce3ff6..a5c66b6a 100644 --- a/src/rust/lqos_support_tool/src/main.rs +++ b/src/rust/lqos_support_tool/src/main.rs @@ -24,6 +24,13 @@ enum Commands { /// The filename to read filename: String }, + /// Expand all submitted data from a support dump into a given directory + Expand { + /// The filename to read from + filename: String, + /// The target directory + target: String, + } } fn gather_dump() { @@ -64,6 +71,45 @@ fn sanity_checks() { } } +fn expand(filename: &str, target: &str) { + // Check inputs + let in_path = Path::new(filename); + if !in_path.exists() { + println!("{} not found", filename); + return; + } + let out_path = Path::new(target); + if !out_path.exists() { + println!("{} not found", target); + return; + } + if !out_path.is_dir() { + println!("{} is not a directory", target); + return; + } + + // Load the data + let bytes = std::fs::read(&in_path).unwrap(); + if let Ok(decoded) = SupportDump::from_bytes(&bytes) { + // Save the sanity check results + let mut sanity = String::new(); + for check in decoded.sanity_checks.results.iter() { + sanity += &format!("{} - Success? {}: {}\n", check.name, check.success, check.comments); + } + let sanity_path = out_path.join("sanity_checks.txt"); + std::fs::write(sanity_path, sanity.as_bytes()).unwrap(); + + // Save the files + for (idx, dump) in decoded.entries.iter().enumerate() { + let trimmed = dump.name.trim().replace(' ', "").to_lowercase().replace('(', "").replace(')', ""); + let dump_path = out_path.join(&format!("{idx:0>2}_{}", trimmed)); + std::fs::write(dump_path, dump.contents.as_bytes()).unwrap(); + } + } + + println!("Expanded data written to {}", target); +} + fn main() { let cli = Cli::parse(); @@ -71,6 +117,7 @@ fn main() { Some(Commands::Sanity) => sanity_checks(), Some(Commands::Gather) => gather_dump(), Some(Commands::Summarize { filename }) => summarize(&filename), + Some(Commands::Expand { filename, target }) => expand(&filename, &target), _ => {} } } \ No newline at end of file From 9293ea59c40480b607cf1d2ae9975ab3670424a1 Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Thu, 13 Jun 2024 10:16:54 -0500 Subject: [PATCH 06/11] Add header data to the support dump. --- src/rust/lqos_support_tool/src/main.rs | 52 ++++++++++++++++++- .../lqos_support_tool/src/support_info.rs | 8 ++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/rust/lqos_support_tool/src/main.rs b/src/rust/lqos_support_tool/src/main.rs index a5c66b6a..56ec592f 100644 --- a/src/rust/lqos_support_tool/src/main.rs +++ b/src/rust/lqos_support_tool/src/main.rs @@ -4,6 +4,8 @@ use std::path::Path; use std::time::{SystemTime, UNIX_EPOCH}; use clap::{Parser, Subcommand}; +use colored::Colorize; +use lqos_config::load_config; use lqos_support_tool::{gather_all_support_info, run_sanity_checks, SupportDump}; #[derive(Parser)] @@ -33,8 +35,47 @@ enum Commands { } } +fn read_line() -> String { + use std::io::{stdin,stdout,Write}; + let mut s = String::new(); + stdin().read_line(&mut s).expect("Did not enter a correct string"); + s.trim().to_string() +} + +fn get_lts_key() -> String { + if let Ok(cfg) = load_config() { + if let Some(key) = cfg.long_term_stats.license_key { + return key.clone(); + } + } + + println!(); + println!("{}", "No LTS Key Found!".bright_red()); + println!("We prioritize helping Long-Term Stats Subscribers and Donors."); + println!("Please enter your LTS key (from your email), or ENTER for none:"); + return read_line(); +} + +fn get_name() -> String { + let mut candidate = String::new(); + while candidate.is_empty() { + println!("Please enter your name, email address and Zulip handle in a single line (ENTER when done)."); + candidate = read_line(); + } + candidate +} + +fn get_comments() -> String { + println!("Anything you'd like to tell us about? (Comments)"); + read_line() +} + fn gather_dump() { - let dump = gather_all_support_info().unwrap(); + let name = get_name(); + let lts_key = get_lts_key(); + let comments = get_comments(); + + let dump = gather_all_support_info(&name, &comments, <s_key).unwrap(); let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); let filename = format!("/tmp/libreqos_{}.support", timestamp); let path = Path::new(&filename); @@ -49,6 +90,10 @@ fn summarize(filename: &str) { } else { let bytes = std::fs::read(&path).unwrap(); if let Ok(decoded) = SupportDump::from_bytes(&bytes) { + println!("Sent by: {}", decoded.sender); + println!("Comments: {}", decoded.comment); + println!("LTS Key: {}", decoded.lts_key); + println!("{:50} {:10} {}", "Sanity Check", "Success?", "Comment"); for entry in decoded.sanity_checks.results.iter() { println!("{:50} {:10} {}", entry.name, entry.success, entry.comments); @@ -91,6 +136,11 @@ fn expand(filename: &str, target: &str) { // Load the data let bytes = std::fs::read(&in_path).unwrap(); if let Ok(decoded) = SupportDump::from_bytes(&bytes) { + // Save the header + let header = format!("From: {}\nComment: {}\nLTS Key: {}\n", decoded.sender, decoded.comment, decoded.lts_key); + let header_path = out_path.join("header.txt"); + std::fs::write(header_path, header.as_bytes()).unwrap(); + // Save the sanity check results let mut sanity = String::new(); for check in decoded.sanity_checks.results.iter() { diff --git a/src/rust/lqos_support_tool/src/support_info.rs b/src/rust/lqos_support_tool/src/support_info.rs index 1cb104df..4fa26b8f 100644 --- a/src/rust/lqos_support_tool/src/support_info.rs +++ b/src/rust/lqos_support_tool/src/support_info.rs @@ -19,6 +19,9 @@ pub trait SupportInfo { #[derive(Debug, Serialize, Deserialize)] pub struct SupportDump { + pub sender: String, + pub comment: String, + pub lts_key: String, pub sanity_checks: SanityChecks, pub entries: Vec } @@ -44,7 +47,7 @@ impl SupportDump { } } -pub fn gather_all_support_info() -> anyhow::Result { +pub fn gather_all_support_info(sender: &str, comments: &str, lts_key: &str) -> anyhow::Result { let sanity_checks = run_sanity_checks()?; let mut data_targets: Vec> = vec![ @@ -72,6 +75,9 @@ pub fn gather_all_support_info() -> anyhow::Result { } let mut dump = SupportDump { + sender: sender.to_string(), + comment: comments.to_string(), + lts_key: lts_key.to_string(), sanity_checks, entries: Vec::new(), }; From fa4d88164eb6e9ba2b8c5fd6d52ea971fc56c79b Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Thu, 13 Jun 2024 13:12:10 -0500 Subject: [PATCH 07/11] Round-trip submission to the server. --- src/rust/lqos_support_tool/src/lib.rs | 29 +++++++++++++++++++++++++- src/rust/lqos_support_tool/src/main.rs | 16 ++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/rust/lqos_support_tool/src/lib.rs b/src/rust/lqos_support_tool/src/lib.rs index 39d45773..06d0932d 100644 --- a/src/rust/lqos_support_tool/src/lib.rs +++ b/src/rust/lqos_support_tool/src/lib.rs @@ -3,6 +3,33 @@ mod support_info; pub mod console; mod sanity_checks; +use std::io::Write; +use std::net::TcpStream; pub use support_info::gather_all_support_info; pub use support_info::SupportDump; -pub use sanity_checks::run_sanity_checks; \ No newline at end of file +pub use sanity_checks::run_sanity_checks; +use crate::console::{error, success}; + +const REMOTE_SYSTEM: &str = "stats.libreqos.io:9200"; + +pub fn submit_to_network(dump: SupportDump) { + // Build the payload + let serialized = dump.serialize_and_compress().unwrap(); + let length = serialized.len() as u64; + let mut bytes = Vec::new(); + bytes.extend(1212u32.to_be_bytes()); + bytes.extend(length.to_be_bytes()); + bytes.extend(&serialized); + bytes.extend(1212u32.to_be_bytes()); + + // Do the actual connection + let stream = TcpStream::connect(REMOTE_SYSTEM); + if stream.is_err() { + error(&format!("Unable to connect to {REMOTE_SYSTEM}")); + println!("{stream:?}"); + return; + } + let mut stream = stream.unwrap(); + stream.write_all(&bytes).unwrap(); + success("Submitted to LibreQoS for Analysis. Thank you."); +} \ No newline at end of file diff --git a/src/rust/lqos_support_tool/src/main.rs b/src/rust/lqos_support_tool/src/main.rs index 56ec592f..20941fe3 100644 --- a/src/rust/lqos_support_tool/src/main.rs +++ b/src/rust/lqos_support_tool/src/main.rs @@ -21,6 +21,8 @@ enum Commands { Sanity, /// Gather Support Info and Save it to /tmp Gather, + /// Gather Support Info and Send it to the LibreQoS Team. Note that LTS users and donors get priority, we don't guarantee that we'll help anyone else. Please make sure you've tried Zulip first ( https://chat.libreqos.io/ ) + Submit, /// Summarize the contents of a support dump Summarize { /// The filename to read @@ -160,6 +162,19 @@ fn expand(filename: &str, target: &str) { println!("Expanded data written to {}", target); } +fn submit() { + // Get header + let name = get_name(); + let lts_key = get_lts_key(); + let comments = get_comments(); + + // Get the data + let dump = gather_all_support_info(&name, &comments, <s_key).unwrap(); + + // Send it + lqos_support_tool::submit_to_network(dump); +} + fn main() { let cli = Cli::parse(); @@ -168,6 +183,7 @@ fn main() { Some(Commands::Gather) => gather_dump(), Some(Commands::Summarize { filename }) => summarize(&filename), Some(Commands::Expand { filename, target }) => expand(&filename, &target), + Some(Commands::Submit) => submit(), _ => {} } } \ No newline at end of file From a74c060a60e1ed49d40733674a2e6e9566196d82 Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Thu, 13 Jun 2024 13:32:14 -0500 Subject: [PATCH 08/11] Add IP Address information --- .../lqos_support_tool/src/support_info.rs | 2 + .../src/support_info/ip_addr.rs | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/rust/lqos_support_tool/src/support_info/ip_addr.rs diff --git a/src/rust/lqos_support_tool/src/support_info.rs b/src/rust/lqos_support_tool/src/support_info.rs index 4fa26b8f..530228b4 100644 --- a/src/rust/lqos_support_tool/src/support_info.rs +++ b/src/rust/lqos_support_tool/src/support_info.rs @@ -9,6 +9,7 @@ mod systemctl_service_single; mod lqos_config; mod task_journal; mod service_config; +mod ip_addr; pub trait SupportInfo { fn get_string(&self) -> String; @@ -53,6 +54,7 @@ pub fn gather_all_support_info(sender: &str, comments: &str, lts_key: &str) -> a let mut data_targets: Vec> = vec![ lqos_config::LqosConfig::boxed(), ip_link::IpLink::boxed(), + ip_addr::IpAddr::boxed(), systemctl_services::SystemCtlServices::boxed(), systemctl_service_single::SystemCtlService::boxed("lqosd"), systemctl_service_single::SystemCtlService::boxed("lqos_node_manager"), diff --git a/src/rust/lqos_support_tool/src/support_info/ip_addr.rs b/src/rust/lqos_support_tool/src/support_info/ip_addr.rs new file mode 100644 index 00000000..3743bc8f --- /dev/null +++ b/src/rust/lqos_support_tool/src/support_info/ip_addr.rs @@ -0,0 +1,38 @@ +use std::process::Command; +use crate::console::success; +use crate::support_info::SupportInfo; + +#[derive(Debug, Default)] +pub struct IpAddr { + output: String, +} + +impl SupportInfo for IpAddr { + fn get_string(&self) -> String { + self.output.to_string() + } + + fn get_name(&self) -> String { + "IP Address Information".to_string() + } + + fn get_filename(&self) -> Option { + None + } + + fn gather(&mut self) -> anyhow::Result<()> { + let output = Command::new("/sbin/ip") + .arg("addr") + .output()?; + let out_str = String::from_utf8_lossy(output.stdout.as_slice()); + self.output = out_str.to_string(); + success("Gathered `ip addr` data"); + Ok(()) + } +} + +impl IpAddr { + pub fn boxed() -> Box { + Box::new(Self::default()) + } +} \ No newline at end of file From 8714a407a181953b3593a1f511dee7c9e255ec91 Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Fri, 14 Jun 2024 10:59:26 -0500 Subject: [PATCH 09/11] Add web UI for support submissions --- src/rust/Cargo.lock | 1 + src/rust/lqos_node_manager/Cargo.toml | 1 + src/rust/lqos_node_manager/src/main.rs | 6 + .../lqos_node_manager/src/static_pages.rs | 8 + src/rust/lqos_node_manager/src/support.rs | 62 ++++ .../static/circuit_queue.html | 3 + src/rust/lqos_node_manager/static/help.html | 322 ++++++++++++++++++ .../lqos_node_manager/static/ip_dump.html | 3 + src/rust/lqos_node_manager/static/main.html | 3 + src/rust/lqos_node_manager/static/shaped.html | 3 + src/rust/lqos_node_manager/static/tree.html | 3 + .../lqos_node_manager/static/unknown-ip.html | 3 + src/rust/lqos_support_tool/src/lib.rs | 1 + 13 files changed, 419 insertions(+) create mode 100644 src/rust/lqos_node_manager/src/support.rs create mode 100644 src/rust/lqos_node_manager/static/help.html diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index 3e5a6cce..5f686d1a 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -1716,6 +1716,7 @@ dependencies = [ "jemallocator", "lqos_bus", "lqos_config", + "lqos_support_tool", "lqos_utils", "nix 0.28.0", "once_cell", diff --git a/src/rust/lqos_node_manager/Cargo.toml b/src/rust/lqos_node_manager/Cargo.toml index 47240699..49410eea 100644 --- a/src/rust/lqos_node_manager/Cargo.toml +++ b/src/rust/lqos_node_manager/Cargo.toml @@ -22,6 +22,7 @@ once_cell = "1" dns-lookup = "1" dashmap = "5" reqwest = { version = "0.11.20", features = ["json"] } +lqos_support_tool = { path = "../lqos_support_tool" } # Support JemAlloc on supported platforms [target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'.dependencies] diff --git a/src/rust/lqos_node_manager/src/main.rs b/src/rust/lqos_node_manager/src/main.rs index 03324f79..b08dc3eb 100644 --- a/src/rust/lqos_node_manager/src/main.rs +++ b/src/rust/lqos_node_manager/src/main.rs @@ -13,6 +13,7 @@ mod network_tree; mod queue_info; mod toasts; mod flow_monitor; +mod support; // Use JemAllocator only on supported platforms #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -45,6 +46,7 @@ fn rocket() -> _ { static_pages::unknown_devices_page, static_pages::circuit_queue, static_pages::pretty_map_graph, + static_pages::help_page, config_control::config_page, network_tree::tree_page, static_pages::ip_dump, @@ -124,6 +126,10 @@ fn rocket() -> _ { flow_monitor::flows_lat_lon, flow_monitor::flows_ether_protocol, flow_monitor::flows_ip_protocol, + // Suport System + support::run_sanity_check, + support::gather_support_data, + support::submit_support_data, ], ); diff --git a/src/rust/lqos_node_manager/src/static_pages.rs b/src/rust/lqos_node_manager/src/static_pages.rs index eae42154..70ceca90 100644 --- a/src/rust/lqos_node_manager/src/static_pages.rs +++ b/src/rust/lqos_node_manager/src/static_pages.rs @@ -83,6 +83,14 @@ pub async fn pretty_map_graph<'a>( NoCache::new(NamedFile::open("static/showoff.html").await.ok()) } +// Help me obi-wan, you're our only hope +#[get("/help")] +pub async fn help_page<'a>( + _auth: AuthGuard, +) -> NoCache> { + NoCache::new(NamedFile::open("static/help.html").await.ok()) +} + #[get("/vendor/bootstrap.min.css")] pub async fn bootsrap_css<'a>() -> LongCache> { LongCache::new(NamedFile::open("static/vendor/bootstrap.min.css").await.ok()) diff --git a/src/rust/lqos_node_manager/src/support.rs b/src/rust/lqos_node_manager/src/support.rs new file mode 100644 index 00000000..85845251 --- /dev/null +++ b/src/rust/lqos_node_manager/src/support.rs @@ -0,0 +1,62 @@ +use std::path::Path; +use std::time::{SystemTime, UNIX_EPOCH}; +use rocket::fs::NamedFile; +use rocket::serde::Deserialize; +use rocket::serde::json::Json; +use lqos_config::load_config; +use lqos_support_tool::{run_sanity_checks, SanityChecks}; +use crate::auth_guard::AuthGuard; + +#[get("/api/sanity")] +pub async fn run_sanity_check( + _auth: AuthGuard, +) -> Json { + let mut status = run_sanity_checks().unwrap(); + status.results.sort_by(|a,b| a.success.cmp(&b.success)); + Json(status) +} + +#[derive(Deserialize, Clone)] +#[serde(crate = "rocket::serde")] +pub struct SupportMetadata { + name: String, + comment: String, +} + +#[post("/api/gatherSupport", data="")] +pub async fn gather_support_data( + info: Json +) -> NamedFile { + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + let filename = format!("/tmp/libreqos_{}.support", timestamp); + let path = Path::new(&filename); + + let lts_key = if let Ok(cfg) = load_config() { + cfg.long_term_stats.license_key.unwrap_or("None".to_string()) + } else { + "None".to_string() + }; + if let Ok(dump) = lqos_support_tool::gather_all_support_info(&info.name, &info.comment, <s_key) { + std::fs::write(&path, dump.serialize_and_compress().unwrap()).unwrap(); + } + + NamedFile::open(path).await.unwrap() +} + +#[post("/api/submitSupport", data="")] +pub async fn submit_support_data( + info: Json +) -> String { + let lts_key = if let Ok(cfg) = load_config() { + cfg.long_term_stats.license_key.unwrap_or("None".to_string()) + } else { + "None".to_string() + }; + if let Ok(dump) = lqos_support_tool::gather_all_support_info(&info.name, &info.comment, <s_key) { + lqos_support_tool::submit_to_network(dump); + "Your support submission has been sent".to_string() + } else { + "Something went wrong".to_string() + } + +} \ No newline at end of file diff --git a/src/rust/lqos_node_manager/static/circuit_queue.html b/src/rust/lqos_node_manager/static/circuit_queue.html index 85a5fad2..abac0db8 100644 --- a/src/rust/lqos_node_manager/static/circuit_queue.html +++ b/src/rust/lqos_node_manager/static/circuit_queue.html @@ -50,6 +50,9 @@ +
  • Reload LibreQoS diff --git a/src/rust/lqos_node_manager/static/help.html b/src/rust/lqos_node_manager/static/help.html new file mode 100644 index 00000000..29d48767 --- /dev/null +++ b/src/rust/lqos_node_manager/static/help.html @@ -0,0 +1,322 @@ + + + + + + + + + + + LibreQoS - Local Node Manager + + + + + + + + + + +
    + +
    +
    +
    +
    +
    Help & Support
    + +

    + + Priority support is given to Long-Term Stats subscribers and donors. Other support is + best effort, with volunteers trying to help as best we can. +

    +

    + + Support LibreQoS Today + +

    +
    +
    +
    + +
    +
    +
    +
    Documentation
    +

    The documentation is a great place to start!

    + Read The LibreQoS Documentation +
    +
    +
    + +
    +
    +
    +
    Chat
    +

    + Connect with other LibreQoS users, and the LibreQoS team on our Zulip Chat System.
    + Zulip chat system. +

    +
    +
    +
    +
    + +
    +
    +
    +
    +
    Tools
    +

    + Sanity check reads your configuration and looks for common issues. This should be + your first step when troubleshooting the system.
    + +

    +

    + Download support data to a local file. You can send this to LibreQoS via the + chat or email (as part of an ongoing support discussion).
    + +

    +

    + Submit support data directly to LibreQoS. Please contact us when you do this, + with detailed information about the problems you are experiencing. Repeatedly hitting this + button will get you slower - or no - service!
    + +

    + +
    +
    +
    +
    + +
    + +
    © 2022-2023, LibreQoE LLC
    + + + + + + + + + + + + \ No newline at end of file diff --git a/src/rust/lqos_node_manager/static/ip_dump.html b/src/rust/lqos_node_manager/static/ip_dump.html index d5517ebf..16865cf4 100644 --- a/src/rust/lqos_node_manager/static/ip_dump.html +++ b/src/rust/lqos_node_manager/static/ip_dump.html @@ -41,6 +41,9 @@
  • +
  • Reload LibreQoS
  • diff --git a/src/rust/lqos_node_manager/static/main.html b/src/rust/lqos_node_manager/static/main.html index 86952fd3..3d765a02 100644 --- a/src/rust/lqos_node_manager/static/main.html +++ b/src/rust/lqos_node_manager/static/main.html @@ -48,6 +48,9 @@ +
  • Reload LibreQoS diff --git a/src/rust/lqos_node_manager/static/shaped.html b/src/rust/lqos_node_manager/static/shaped.html index cbd9dccd..1b47c2ee 100644 --- a/src/rust/lqos_node_manager/static/shaped.html +++ b/src/rust/lqos_node_manager/static/shaped.html @@ -41,6 +41,9 @@
  • +
  • Reload LibreQoS
  • diff --git a/src/rust/lqos_node_manager/static/tree.html b/src/rust/lqos_node_manager/static/tree.html index c7bee389..013ced6f 100644 --- a/src/rust/lqos_node_manager/static/tree.html +++ b/src/rust/lqos_node_manager/static/tree.html @@ -48,6 +48,9 @@ +
  • Reload LibreQoS diff --git a/src/rust/lqos_node_manager/static/unknown-ip.html b/src/rust/lqos_node_manager/static/unknown-ip.html index e9302e21..8470eff7 100644 --- a/src/rust/lqos_node_manager/static/unknown-ip.html +++ b/src/rust/lqos_node_manager/static/unknown-ip.html @@ -40,6 +40,9 @@
  • +
  • Reload LibreQoS
  • diff --git a/src/rust/lqos_support_tool/src/lib.rs b/src/rust/lqos_support_tool/src/lib.rs index 06d0932d..e2b6d6d6 100644 --- a/src/rust/lqos_support_tool/src/lib.rs +++ b/src/rust/lqos_support_tool/src/lib.rs @@ -9,6 +9,7 @@ pub use support_info::gather_all_support_info; pub use support_info::SupportDump; pub use sanity_checks::run_sanity_checks; use crate::console::{error, success}; +pub use sanity_checks::SanityChecks; const REMOTE_SYSTEM: &str = "stats.libreqos.io:9200"; From 414f78c1d3ab86799962cd9ad314334b095f10cb Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Fri, 14 Jun 2024 13:41:16 -0500 Subject: [PATCH 10/11] Add grabbing distro and kernel info --- src/rust/lqos_support_tool/src/sanity_checks.rs | 2 +- src/rust/lqos_support_tool/src/support_info.rs | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/rust/lqos_support_tool/src/sanity_checks.rs b/src/rust/lqos_support_tool/src/sanity_checks.rs index d2d879c1..447dfd72 100644 --- a/src/rust/lqos_support_tool/src/sanity_checks.rs +++ b/src/rust/lqos_support_tool/src/sanity_checks.rs @@ -8,7 +8,7 @@ mod shaped_devices; use serde::{Deserialize, Serialize}; use crate::console::{error, success}; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Default)] pub struct SanityChecks { pub results: Vec, } diff --git a/src/rust/lqos_support_tool/src/support_info.rs b/src/rust/lqos_support_tool/src/support_info.rs index 530228b4..4aa9f8b3 100644 --- a/src/rust/lqos_support_tool/src/support_info.rs +++ b/src/rust/lqos_support_tool/src/support_info.rs @@ -10,6 +10,8 @@ mod lqos_config; mod task_journal; mod service_config; mod ip_addr; +mod kernel_info; +mod distro_name; pub trait SupportInfo { fn get_string(&self) -> String; @@ -18,7 +20,7 @@ pub trait SupportInfo { fn gather(&mut self) -> anyhow::Result<()>; } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Default)] pub struct SupportDump { pub sender: String, pub comment: String, @@ -27,7 +29,7 @@ pub struct SupportDump { pub entries: Vec } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Default)] pub struct DumpEntry { pub name: String, pub filename: Option, @@ -55,6 +57,8 @@ pub fn gather_all_support_info(sender: &str, comments: &str, lts_key: &str) -> a lqos_config::LqosConfig::boxed(), ip_link::IpLink::boxed(), ip_addr::IpAddr::boxed(), + kernel_info::KernelInfo::boxed(), + distro_name::DistroName::boxed(), systemctl_services::SystemCtlServices::boxed(), systemctl_service_single::SystemCtlService::boxed("lqosd"), systemctl_service_single::SystemCtlService::boxed("lqos_node_manager"), @@ -108,9 +112,10 @@ mod tests { DumpEntry { name: "Test".to_string(), filename: None, - contents: "BLAH".to_string(), + contents: "BLAH".to_string(), } - ] + ], + ..Default::default() }; let bytes = original.serialize_and_compress().unwrap(); let restored = SupportDump::from_bytes(&bytes).unwrap(); From bc60689984d0db64e20792d62e79b4b4f20960f9 Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Fri, 14 Jun 2024 15:07:26 -0500 Subject: [PATCH 11/11] Missed two files from previous commit --- .../src/support_info/distro_name.rs | 38 +++++++++++++++++++ .../src/support_info/kernel_info.rs | 38 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 src/rust/lqos_support_tool/src/support_info/distro_name.rs create mode 100644 src/rust/lqos_support_tool/src/support_info/kernel_info.rs diff --git a/src/rust/lqos_support_tool/src/support_info/distro_name.rs b/src/rust/lqos_support_tool/src/support_info/distro_name.rs new file mode 100644 index 00000000..c277db09 --- /dev/null +++ b/src/rust/lqos_support_tool/src/support_info/distro_name.rs @@ -0,0 +1,38 @@ +use std::process::Command; +use crate::console::success; +use crate::support_info::SupportInfo; + +#[derive(Debug, Default)] +pub struct DistroName { + output: String, +} + +impl SupportInfo for DistroName { + fn get_string(&self) -> String { + self.output.to_string() + } + + fn get_name(&self) -> String { + "LSB Distro Info".to_string() + } + + fn get_filename(&self) -> Option { + None + } + + fn gather(&mut self) -> anyhow::Result<()> { + let output = Command::new("/bin/lsb_release") + .arg("-a") + .output()?; + let out_str = String::from_utf8_lossy(output.stdout.as_slice()); + self.output = out_str.to_string(); + success("Gathered distro info"); + Ok(()) + } +} + +impl DistroName { + pub fn boxed() -> Box { + Box::new(Self::default()) + } +} \ No newline at end of file diff --git a/src/rust/lqos_support_tool/src/support_info/kernel_info.rs b/src/rust/lqos_support_tool/src/support_info/kernel_info.rs new file mode 100644 index 00000000..3bae10c7 --- /dev/null +++ b/src/rust/lqos_support_tool/src/support_info/kernel_info.rs @@ -0,0 +1,38 @@ +use std::process::Command; +use crate::console::success; +use crate::support_info::SupportInfo; + +#[derive(Debug, Default)] +pub struct KernelInfo { + output: String, +} + +impl SupportInfo for KernelInfo { + fn get_string(&self) -> String { + self.output.to_string() + } + + fn get_name(&self) -> String { + "Uname Kernel Info".to_string() + } + + fn get_filename(&self) -> Option { + None + } + + fn gather(&mut self) -> anyhow::Result<()> { + let output = Command::new("/bin/uname") + .arg("-a") + .output()?; + let out_str = String::from_utf8_lossy(output.stdout.as_slice()); + self.output = out_str.to_string(); + success("Gathered kernel info"); + Ok(()) + } +} + +impl KernelInfo { + pub fn boxed() -> Box { + Box::new(Self::default()) + } +} \ No newline at end of file