From 822dcb9530419693c7bc6997a36576520a0e36e1 Mon Sep 17 00:00:00 2001 From: Emmanuel Bosquet Date: Mon, 16 Oct 2023 15:34:57 +0200 Subject: [PATCH 1/4] CLI: all responses are displayable in JSON --- bin/src/cli.rs | 48 +++++-------------- bin/src/ctl/command.rs | 84 ++++++++++++---------------------- bin/src/ctl/display.rs | 47 +++++++++++++------ bin/src/ctl/mod.rs | 18 +++++--- bin/src/ctl/request_builder.rs | 16 +++---- 5 files changed, 94 insertions(+), 119 deletions(-) diff --git a/bin/src/cli.rs b/bin/src/cli.rs index 454dc9dbd..28497a978 100644 --- a/bin/src/cli.rs +++ b/bin/src/cli.rs @@ -24,6 +24,13 @@ pub struct Args { help = "Sets a custom timeout for commands (in milliseconds). 0 disables the timeout" )] pub timeout: Option, + #[clap( + short = 'j', + long = "json", + global = true, + help = "display responses to queries in a JSON format" + )] + pub json: bool, #[clap(subcommand)] pub cmd: SubCmd, } @@ -109,26 +116,12 @@ pub enum SubCmd { }, #[clap(name = "status", about = "gets information on the running workers")] - Status { - #[clap( - short = 'j', - long = "json", - help = "Print the command result in JSON format" - )] - json: bool, - }, + Status, #[clap( name = "metrics", about = "gets statistics on the main process and its workers" )] Metrics { - #[clap( - short = 'j', - long = "json", - help = "Print the command result in JSON format", - global = true - )] - json: bool, #[clap(subcommand)] cmd: MetricsCmd, }, @@ -153,22 +146,9 @@ pub enum SubCmd { help = "use a different configuration file from the current one" )] file: Option, - #[clap( - short = 'j', - long = "json", - help = "Print the command result in JSON format" - )] - json: bool, }, #[clap(name = "cluster", about = "cluster management")] Cluster { - #[clap( - short = 'j', - long = "json", - help = "Print the command result in JSON format", - global = true - )] - json: bool, #[clap(subcommand)] cmd: ClusterCmd, }, @@ -189,13 +169,6 @@ pub enum SubCmd { }, #[clap(name = "certificate", about = "list, add and remove certificates")] Certificate { - #[clap( - short = 'j', - long = "json", - help = "Print the command result in JSON format", - global = true - )] - json: bool, #[clap(subcommand)] cmd: CertificateCmd, }, @@ -216,7 +189,10 @@ pub enum MetricsCmd { Disable, #[clap(name = "clear", about = "Deletes local metrics data")] Clear, - #[clap(name = "get", about = "get all or filtered metrics")] + #[clap( + name = "get", + about = "get all metrics, filtered, or a list of available metrics" + )] Get { #[clap(short, long, help = "list the available metrics on the proxy level")] list: bool, diff --git a/bin/src/ctl/command.rs b/bin/src/ctl/command.rs index d291b1e3f..bf45c9178 100644 --- a/bin/src/ctl/command.rs +++ b/bin/src/ctl/command.rs @@ -1,11 +1,10 @@ use anyhow::{self, bail, Context}; use prettytable::Table; -use serde::Serialize; use sozu_command_lib::proto::command::{ request::RequestType, response_content::ContentType, ListWorkers, QueryCertificatesFilters, QueryClusterByDomain, QueryClustersHashes, QueryMetricsOptions, Request, Response, - ResponseContent, ResponseStatus, RunState, UpgradeMain, WorkerInfo, + ResponseContent, ResponseStatus, RunState, UpgradeMain, }; use crate::ctl::{ @@ -18,13 +17,6 @@ use crate::ctl::{ CommandManager, }; -// Used to display the JSON response of the status command -#[derive(Serialize, Debug)] -struct WorkerStatus<'a> { - pub worker: &'a WorkerInfo, - pub status: &'a String, -} - impl CommandManager { fn write_request_on_channel(&mut self, request: Request) -> anyhow::Result<()> { self.channel @@ -39,14 +31,6 @@ impl CommandManager { } pub fn send_request(&mut self, request: Request) -> Result<(), anyhow::Error> { - self.send_request_to_workers(request, false) - } - - pub fn send_request_to_workers( - &mut self, - request: Request, - json: bool, - ) -> Result<(), anyhow::Error> { self.channel .write_message(&request) .with_context(|| "Could not write the request")?; @@ -60,29 +44,22 @@ impl CommandManager { } ResponseStatus::Failure => bail!("Request failed: {}", response.message), ResponseStatus::Ok => { - if json { - // why do we need to print a success message in json? - print_json_response(&response.message)?; - } else { - println!("{}", response.message); - } + println!("{}", response.message); if let Some(response_content) = response.content { match response_content.content_type { Some(ContentType::RequestCounts(request_counts)) => { - print_request_counts(&request_counts) + print_request_counts(&request_counts, self.json)?; } Some(ContentType::FrontendList(frontends)) => { - print_frontend_list(frontends) + print_frontend_list(frontends, self.json)?; } Some(ContentType::Workers(worker_infos)) => { - if json { - print_json_response(&worker_infos)?; - } else { - print_status(worker_infos); - } + print_status(worker_infos, self.json)?; + } + Some(ContentType::ListenersList(list)) => { + print_listeners(list, self.json)?; } - Some(ContentType::ListenersList(list)) => print_listeners(list), _ => {} } } @@ -220,7 +197,6 @@ impl CommandManager { pub fn get_metrics( &mut self, - json: bool, list: bool, refresh: Option, metric_names: Vec, @@ -250,7 +226,7 @@ impl CommandManager { debug!("Proxy is processing: {}", response.message); } ResponseStatus::Failure => { - if json { + if self.json { return print_json_response(&response.message); } else { bail!("could not query proxy state: {}", response.message); @@ -260,10 +236,10 @@ impl CommandManager { if let Some(response_content) = response.content { match response_content.content_type { Some(ContentType::Metrics(aggregated_metrics_data)) => { - print_metrics(aggregated_metrics_data, json)? + print_metrics(aggregated_metrics_data, self.json)? } Some(ContentType::AvailableMetrics(available)) => { - print_available_metrics(&available)?; + print_available_metrics(&available, self.json)?; } _ => { debug!("Wrong kind of response here"); @@ -293,7 +269,6 @@ impl CommandManager { pub fn query_cluster( &mut self, - json: bool, cluster_id: Option, domain: Option, ) -> Result<(), anyhow::Error> { @@ -334,17 +309,22 @@ impl CommandManager { debug!("Proxy is processing: {}", response.message); } ResponseStatus::Failure => { - if json { + if self.json { print_json_response(&response.message)?; } bail!("could not query proxy state: {}", response.message); } ResponseStatus::Ok => { - if let Some(ResponseContent { - content_type: Some(ContentType::WorkerResponses(worker_responses)), - }) = response.content - { - print_cluster_responses(cluster_id, domain, worker_responses, json)? + match response.content { + Some(ResponseContent { + content_type: Some(ContentType::WorkerResponses(worker_responses)), + }) => print_cluster_responses( + cluster_id, + domain, + worker_responses, + self.json, + )?, + _ => bail!("Wrong response content"), } break; } @@ -356,7 +336,6 @@ impl CommandManager { pub fn query_certificates( &mut self, - json: bool, fingerprint: Option, domain: Option, query_workers: bool, @@ -367,15 +346,14 @@ impl CommandManager { }; if query_workers { - self.query_certificates_from_workers(json, filters) + self.query_certificates_from_workers(filters) } else { - self.query_certificates_from_the_state(json, filters) + self.query_certificates_from_the_state(filters) } } fn query_certificates_from_workers( &mut self, - json: bool, filters: QueryCertificatesFilters, ) -> Result<(), anyhow::Error> { self.write_request_on_channel(RequestType::QueryCertificatesFromWorkers(filters).into())?; @@ -388,19 +366,17 @@ impl CommandManager { debug!("Proxy is processing: {}", response.message); } ResponseStatus::Failure => { - if json { + if self.json { print_json_response(&response.message)?; - bail!("We received an error message"); - } else { - bail!("could not get certificate: {}", response.message); } + bail!("could not get certificate: {}", response.message); } ResponseStatus::Ok => { info!("We did get a response from the proxy"); match response.content { Some(ResponseContent { content_type: Some(ContentType::WorkerResponses(worker_responses)), - }) => print_certificates_by_worker(worker_responses.map, json)?, + }) => print_certificates_by_worker(worker_responses.map, self.json)?, _ => bail!("unexpected response: {:?}", response.content), } break; @@ -412,7 +388,6 @@ impl CommandManager { fn query_certificates_from_the_state( &mut self, - json: bool, filters: QueryCertificatesFilters, ) -> anyhow::Result<()> { self.write_request_on_channel(RequestType::QueryCertificatesFromTheState(filters).into())?; @@ -440,9 +415,8 @@ impl CommandManager { bail!("No certificates match your request."); } - if json { - print_json_response(&certs) - .with_context(|| "Could not print certificates in JSON")?; + if self.json { + print_json_response(&certs)?; } else { print_certificates_with_validity(certs) .with_context(|| "Could not show certificate")?; diff --git a/bin/src/ctl/display.rs b/bin/src/ctl/display.rs index 78a8eb66a..dea9dcad5 100644 --- a/bin/src/ctl/display.rs +++ b/bin/src/ctl/display.rs @@ -2,6 +2,8 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use anyhow::{self, Context}; use prettytable::{Row, Table}; +use time::format_description; +use x509_parser::time::ASN1Time; use sozu_command_lib::proto::{ command::{ @@ -12,10 +14,11 @@ use sozu_command_lib::proto::{ }, display::concatenate_vector, }; -use time::format_description; -use x509_parser::time::ASN1Time; -pub fn print_listeners(listeners_list: ListenersList) { +pub fn print_listeners(listeners_list: ListenersList, json: bool) -> anyhow::Result<()> { + if json { + return print_json_response(&listeners_list); + } println!("\nHTTP LISTENERS\n================"); for (_, http_listener) in listeners_list.http_listeners.iter() { @@ -117,9 +120,13 @@ pub fn print_listeners(listeners_list: ListenersList) { } table.printstd(); } + Ok(()) } -pub fn print_status(worker_infos: WorkerInfos) { +pub fn print_status(worker_infos: WorkerInfos, json: bool) -> anyhow::Result<()> { + if json { + return print_json_response(&worker_infos); + } let mut table = Table::new(); table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); table.add_row(row!["worker id", "pid", "run state"]); @@ -130,9 +137,13 @@ pub fn print_status(worker_infos: WorkerInfos) { } table.printstd(); + Ok(()) } -pub fn print_frontend_list(frontends: ListedFrontends) { +pub fn print_frontend_list(frontends: ListedFrontends, json: bool) -> anyhow::Result<()> { + if json { + return print_json_response(&frontends); + } trace!(" We received this frontends to display {:#?}", frontends); // HTTP frontends if !frontends.http_frontends.is_empty() { @@ -211,6 +222,7 @@ pub fn print_frontend_list(frontends: ListedFrontends) { } table.printstd(); } + Ok(()) } pub fn print_metrics( @@ -416,11 +428,11 @@ pub fn print_cluster_responses( worker_responses: WorkerResponses, json: bool, ) -> anyhow::Result<()> { - if let Some(needle) = cluster_id.or(domain) { - if json { - return print_json_response(&worker_responses); - } + if json { + return print_json_response(&worker_responses); + } + if let Some(needle) = cluster_id.or(domain) { let mut cluster_table = create_cluster_table( vec!["id", "sticky_session", "https_redirect"], &worker_responses.map, @@ -654,8 +666,7 @@ pub fn print_certificates_by_worker( json: bool, ) -> anyhow::Result<()> { if json { - print_json_response(&response_contents)?; - return Ok(()); + return print_json_response(&response_contents); } for (worker_id, response_content) in response_contents.iter() { @@ -690,7 +701,13 @@ fn format_tags_to_string(tags: &BTreeMap) -> String { .join(", ") } -pub fn print_available_metrics(available_metrics: &AvailableMetrics) -> anyhow::Result<()> { +pub fn print_available_metrics( + available_metrics: &AvailableMetrics, + json: bool, +) -> anyhow::Result<()> { + if json { + return print_json_response(&available_metrics); + } println!("Available metrics on the proxy level:"); for metric_name in &available_metrics.proxy_metrics { println!("\t{metric_name}"); @@ -746,7 +763,10 @@ pub fn print_certificates_with_validity( Ok(()) } -pub fn print_request_counts(request_counts: &RequestCounts) { +pub fn print_request_counts(request_counts: &RequestCounts, json: bool) -> anyhow::Result<()> { + if json { + return print_json_response(&request_counts); + } let mut table = Table::new(); table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); table.add_row(row!["request type", "count"]); @@ -755,6 +775,7 @@ pub fn print_request_counts(request_counts: &RequestCounts) { table.add_row(row!(request_type, count)); } table.printstd(); + Ok(()) } // ISO 8601 diff --git a/bin/src/ctl/mod.rs b/bin/src/ctl/mod.rs index e80c137e9..ceeb00862 100644 --- a/bin/src/ctl/mod.rs +++ b/bin/src/ctl/mod.rs @@ -22,6 +22,8 @@ pub struct CommandManager { channel: Channel, timeout: Duration, config: Config, + /// wether to display the response in JSON + json: bool, } pub fn ctl(args: cli::Args) -> anyhow::Result<()> { @@ -49,6 +51,7 @@ pub fn ctl(args: cli::Args) -> anyhow::Result<()> { channel, timeout, config, + json: args.json, }; command_manager.handle_command(args.cmd) @@ -56,6 +59,7 @@ pub fn ctl(args: cli::Args) -> anyhow::Result<()> { impl CommandManager { fn handle_command(&mut self, command: SubCmd) -> anyhow::Result<()> { + debug!("Executing command {:?}", command); match command { SubCmd::Shutdown { hard } => { if hard { @@ -68,15 +72,15 @@ impl CommandManager { None => self.upgrade_main(), Some(worker_id) => self.upgrade_worker(worker_id), }, - SubCmd::Status { json } => self.status(json), - SubCmd::Metrics { cmd, json } => match cmd { + SubCmd::Status {} => self.status(), + SubCmd::Metrics { cmd } => match cmd { MetricsCmd::Get { list, refresh, names, clusters, backends, - } => self.get_metrics(json, list, refresh, names, clusters, backends), + } => self.get_metrics(list, refresh, names, clusters, backends), _ => self.configure_metrics(cmd), }, SubCmd::Logging { level } => self.logging_filter(&level), @@ -85,8 +89,8 @@ impl CommandManager { StateCmd::Load { file } => self.load_state(file), StateCmd::Stats => self.count_requests(), }, - SubCmd::Reload { file, json } => self.reload_configuration(file, json), - SubCmd::Cluster { cmd, json } => self.cluster_command(cmd, json), + SubCmd::Reload { file } => self.reload_configuration(file), + SubCmd::Cluster { cmd } => self.cluster_command(cmd), SubCmd::Backend { cmd } => self.backend_command(cmd), SubCmd::Frontend { cmd } => match cmd { FrontendCmd::Http { cmd } => self.http_frontend_command(cmd), @@ -105,7 +109,7 @@ impl CommandManager { ListenerCmd::Tcp { cmd } => self.tcp_listener_command(cmd), ListenerCmd::List => self.list_listeners(), }, - SubCmd::Certificate { cmd, json } => match cmd { + SubCmd::Certificate { cmd } => match cmd { CertificateCmd::Add { certificate, chain, @@ -149,7 +153,7 @@ impl CommandManager { fingerprint, domain, query_workers, - } => self.query_certificates(json, fingerprint, domain, query_workers), + } => self.query_certificates(fingerprint, domain, query_workers), }, SubCmd::Config { cmd: _ } => Ok(()), // noop, handled at the beginning of the method SubCmd::Events => self.events(), diff --git a/bin/src/ctl/request_builder.rs b/bin/src/ctl/request_builder.rs index ba9706dd0..da0151d43 100644 --- a/bin/src/ctl/request_builder.rs +++ b/bin/src/ctl/request_builder.rs @@ -42,19 +42,19 @@ impl CommandManager { pub fn soft_stop(&mut self) -> anyhow::Result<()> { debug!("shutting down proxy softly"); - self.send_request_to_workers(RequestType::SoftStop(SoftStop {}).into(), false) + self.send_request(RequestType::SoftStop(SoftStop {}).into()) } pub fn hard_stop(&mut self) -> anyhow::Result<()> { debug!("shutting down proxy the hard way"); - self.send_request_to_workers(RequestType::HardStop(HardStop {}).into(), false) + self.send_request(RequestType::HardStop(HardStop {}).into()) } - pub fn status(&mut self, json: bool) -> anyhow::Result<()> { + pub fn status(&mut self) -> anyhow::Result<()> { debug!("Requesting status…"); - self.send_request_to_workers(RequestType::Status(Status {}).into(), json) + self.send_request(RequestType::Status(Status {}).into()) } pub fn configure_metrics(&mut self, cmd: MetricsCmd) -> anyhow::Result<()> { @@ -70,13 +70,13 @@ impl CommandManager { self.send_request(RequestType::ConfigureMetrics(configuration as i32).into()) } - pub fn reload_configuration(&mut self, path: Option, json: bool) -> anyhow::Result<()> { + pub fn reload_configuration(&mut self, path: Option) -> anyhow::Result<()> { debug!("Reloading configuration…"); let path = match path { Some(p) => p, None => String::new(), }; - self.send_request_to_workers(RequestType::ReloadConfiguration(path).into(), json) + self.send_request(RequestType::ReloadConfiguration(path).into()) } pub fn list_frontends( @@ -137,7 +137,7 @@ impl CommandManager { } } - pub fn cluster_command(&mut self, cmd: ClusterCmd, json: bool) -> anyhow::Result<()> { + pub fn cluster_command(&mut self, cmd: ClusterCmd) -> anyhow::Result<()> { match cmd { ClusterCmd::Add { id, @@ -166,7 +166,7 @@ impl CommandManager { ) } ClusterCmd::Remove { id } => self.send_request(RequestType::RemoveCluster(id).into()), - ClusterCmd::List { id, domain } => self.query_cluster(json, id, domain), + ClusterCmd::List { id, domain } => self.query_cluster(id, domain), } } From 0e62ff3ca80bbb4b78c533ef9723b9fc19e8ce66 Mon Sep 17 00:00:00 2001 From: Emmanuel Bosquet Date: Tue, 17 Oct 2023 02:32:56 +0200 Subject: [PATCH 2/4] refactor cli display by creating Response::display remove bin/cli/display.rs since it is ported add json field to CommandManager add global json option to CLI arguments put certificate loading functions in certificate module --- Cargo.lock | 3 +- bin/Cargo.toml | 2 - bin/src/ctl/command.rs | 238 +--------- bin/src/ctl/display.rs | 789 ------------------------------- bin/src/ctl/mod.rs | 2 - bin/src/ctl/request_builder.rs | 110 +++-- bin/src/main.rs | 2 - command/Cargo.toml | 1 + command/src/certificate.rs | 67 ++- command/src/command.proto | 1 + command/src/proto/display.rs | 839 ++++++++++++++++++++++++++++++++- command/src/proto/mod.rs | 16 + 12 files changed, 985 insertions(+), 1085 deletions(-) delete mode 100644 bin/src/ctl/display.rs diff --git a/Cargo.lock b/Cargo.lock index cfc7fd0bb..ad969c378 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1902,7 +1902,6 @@ dependencies = [ "nom", "num_cpus", "paw", - "prettytable-rs", "regex", "serde", "serde_json", @@ -1912,7 +1911,6 @@ dependencies = [ "tempfile", "termion", "time", - "x509-parser", ] [[package]] @@ -1928,6 +1926,7 @@ dependencies = [ "nom", "pool", "poule", + "prettytable-rs", "prost", "prost-build", "rand", diff --git a/bin/Cargo.toml b/bin/Cargo.toml index 2e564ffdf..1585e90f5 100644 --- a/bin/Cargo.toml +++ b/bin/Cargo.toml @@ -37,7 +37,6 @@ mio = { version = "^0.8.8", features = ["os-poll", "net"] } nix = { version = "^0.27.1", features = ["signal", "fs"] } nom = "^7.1.3" paw = "^1.0.0" -prettytable-rs = { version = "^0.10.0", default-features = false } serde = { version = "^1.0.188", features = ["derive"] } serde_json = "^1.0.107" time = "^0.3.29" @@ -45,7 +44,6 @@ regex = "^1.10.0" smol = "^1.3.0" tempfile = "^3.8.0" termion = "^2.0.1" -x509-parser = "^0.15.1" sozu-command-lib = { path = "../command", version = "^0.15.17" } sozu-lib = { path = "../lib", version = "^0.15.17" } diff --git a/bin/src/ctl/command.rs b/bin/src/ctl/command.rs index bf45c9178..adb069133 100644 --- a/bin/src/ctl/command.rs +++ b/bin/src/ctl/command.rs @@ -1,21 +1,11 @@ use anyhow::{self, bail, Context}; -use prettytable::Table; use sozu_command_lib::proto::command::{ - request::RequestType, response_content::ContentType, ListWorkers, QueryCertificatesFilters, - QueryClusterByDomain, QueryClustersHashes, QueryMetricsOptions, Request, Response, - ResponseContent, ResponseStatus, RunState, UpgradeMain, + request::RequestType, response_content::ContentType, ListWorkers, QueryMetricsOptions, Request, + Response, ResponseContent, ResponseStatus, RunState, UpgradeMain, }; -use crate::ctl::{ - create_channel, - display::{ - print_available_metrics, print_certificates_by_worker, print_certificates_with_validity, - print_cluster_responses, print_frontend_list, print_json_response, print_listeners, - print_metrics, print_request_counts, print_status, - }, - CommandManager, -}; +use crate::ctl::{create_channel, CommandManager}; impl CommandManager { fn write_request_on_channel(&mut self, request: Request) -> anyhow::Result<()> { @@ -44,25 +34,7 @@ impl CommandManager { } ResponseStatus::Failure => bail!("Request failed: {}", response.message), ResponseStatus::Ok => { - println!("{}", response.message); - - if let Some(response_content) = response.content { - match response_content.content_type { - Some(ContentType::RequestCounts(request_counts)) => { - print_request_counts(&request_counts, self.json)?; - } - Some(ContentType::FrontendList(frontends)) => { - print_frontend_list(frontends, self.json)?; - } - Some(ContentType::Workers(worker_infos)) => { - print_status(worker_infos, self.json)?; - } - Some(ContentType::ListenersList(list)) => { - print_listeners(list, self.json)?; - } - _ => {} - } - } + response.display(self.json)?; break; } } @@ -96,17 +68,8 @@ impl CommandManager { content_type: Some(ContentType::Workers(ref worker_infos)), }) = response.content { - let mut table = Table::new(); - table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); - table.add_row(row!["Worker", "pid", "run state"]); - for worker in worker_infos.vec.iter() { - let run_state = format!("{:?}", worker.run_state); - table.add_row(row![worker.id, worker.pid, run_state]); - } - - println!(); - table.printstd(); - println!(); + // display worker status + response.display(false)?; self.write_request_on_channel( RequestType::UpgradeMain(UpgradeMain {}).into(), @@ -225,28 +188,8 @@ impl CommandManager { ResponseStatus::Processing => { debug!("Proxy is processing: {}", response.message); } - ResponseStatus::Failure => { - if self.json { - return print_json_response(&response.message); - } else { - bail!("could not query proxy state: {}", response.message); - } - } - ResponseStatus::Ok => { - if let Some(response_content) = response.content { - match response_content.content_type { - Some(ContentType::Metrics(aggregated_metrics_data)) => { - print_metrics(aggregated_metrics_data, self.json)? - } - Some(ContentType::AvailableMetrics(available)) => { - print_available_metrics(&available, self.json)?; - } - _ => { - debug!("Wrong kind of response here"); - } - } - } - + ResponseStatus::Failure | ResponseStatus::Ok => { + response.display(self.json)?; break; } } @@ -266,169 +209,4 @@ impl CommandManager { Ok(()) } - - pub fn query_cluster( - &mut self, - cluster_id: Option, - domain: Option, - ) -> Result<(), anyhow::Error> { - if cluster_id.is_some() && domain.is_some() { - bail!("Error: Either request an cluster ID or a domain name"); - } - - let request = if let Some(ref cluster_id) = cluster_id { - RequestType::QueryClusterById(cluster_id.to_string()).into() - } else if let Some(ref domain) = domain { - let splitted: Vec = - domain.splitn(2, '/').map(|elem| elem.to_string()).collect(); - - if splitted.is_empty() { - bail!("Domain can't be empty"); - } - - let query_domain = QueryClusterByDomain { - hostname: splitted - .get(0) - .with_context(|| "Domain can't be empty")? - .clone(), - path: splitted.get(1).cloned().map(|path| format!("/{path}")), // We add the / again because of the splitn removing it - }; - - RequestType::QueryClustersByDomain(query_domain).into() - } else { - RequestType::QueryClustersHashes(QueryClustersHashes {}).into() - }; - - self.write_request_on_channel(request)?; - - loop { - let response = self.read_channel_message_with_timeout()?; - - match response.status() { - ResponseStatus::Processing => { - debug!("Proxy is processing: {}", response.message); - } - ResponseStatus::Failure => { - if self.json { - print_json_response(&response.message)?; - } - bail!("could not query proxy state: {}", response.message); - } - ResponseStatus::Ok => { - match response.content { - Some(ResponseContent { - content_type: Some(ContentType::WorkerResponses(worker_responses)), - }) => print_cluster_responses( - cluster_id, - domain, - worker_responses, - self.json, - )?, - _ => bail!("Wrong response content"), - } - break; - } - } - } - - Ok(()) - } - - pub fn query_certificates( - &mut self, - fingerprint: Option, - domain: Option, - query_workers: bool, - ) -> Result<(), anyhow::Error> { - let filters = QueryCertificatesFilters { - domain, - fingerprint, - }; - - if query_workers { - self.query_certificates_from_workers(filters) - } else { - self.query_certificates_from_the_state(filters) - } - } - - fn query_certificates_from_workers( - &mut self, - filters: QueryCertificatesFilters, - ) -> Result<(), anyhow::Error> { - self.write_request_on_channel(RequestType::QueryCertificatesFromWorkers(filters).into())?; - - loop { - let response = self.read_channel_message_with_timeout()?; - - match response.status() { - ResponseStatus::Processing => { - debug!("Proxy is processing: {}", response.message); - } - ResponseStatus::Failure => { - if self.json { - print_json_response(&response.message)?; - } - bail!("could not get certificate: {}", response.message); - } - ResponseStatus::Ok => { - info!("We did get a response from the proxy"); - match response.content { - Some(ResponseContent { - content_type: Some(ContentType::WorkerResponses(worker_responses)), - }) => print_certificates_by_worker(worker_responses.map, self.json)?, - _ => bail!("unexpected response: {:?}", response.content), - } - break; - } - } - } - Ok(()) - } - - fn query_certificates_from_the_state( - &mut self, - filters: QueryCertificatesFilters, - ) -> anyhow::Result<()> { - self.write_request_on_channel(RequestType::QueryCertificatesFromTheState(filters).into())?; - - loop { - let response = self.read_channel_message_with_timeout()?; - - match response.status() { - ResponseStatus::Processing => { - debug!("Proxy is processing: {}", response.message); - } - ResponseStatus::Failure => { - bail!("could not get certificate: {}", response.message); - } - ResponseStatus::Ok => { - debug!("We did get a response from the proxy"); - trace!("response message: {:?}\n", response.message); - - if let Some(response_content) = response.content { - let certs = match response_content.content_type { - Some(ContentType::CertificatesWithFingerprints(certs)) => certs.certs, - _ => bail!(format!("Wrong response content {:?}", response_content)), - }; - if certs.is_empty() { - bail!("No certificates match your request."); - } - - if self.json { - print_json_response(&certs)?; - } else { - print_certificates_with_validity(certs) - .with_context(|| "Could not show certificate")?; - } - } else { - debug!("No response content."); - } - - break; - } - } - } - Ok(()) - } } diff --git a/bin/src/ctl/display.rs b/bin/src/ctl/display.rs deleted file mode 100644 index dea9dcad5..000000000 --- a/bin/src/ctl/display.rs +++ /dev/null @@ -1,789 +0,0 @@ -use std::collections::{BTreeMap, HashMap, HashSet}; - -use anyhow::{self, Context}; -use prettytable::{Row, Table}; -use time::format_description; -use x509_parser::time::ASN1Time; - -use sozu_command_lib::proto::{ - command::{ - filtered_metrics, response_content::ContentType, AggregatedMetrics, AvailableMetrics, - CertificateAndKey, CertificatesWithFingerprints, ClusterMetrics, FilteredMetrics, - ListedFrontends, ListenersList, RequestCounts, ResponseContent, WorkerInfos, WorkerMetrics, - WorkerResponses, - }, - display::concatenate_vector, -}; - -pub fn print_listeners(listeners_list: ListenersList, json: bool) -> anyhow::Result<()> { - if json { - return print_json_response(&listeners_list); - } - println!("\nHTTP LISTENERS\n================"); - - for (_, http_listener) in listeners_list.http_listeners.iter() { - let mut table = Table::new(); - table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); - table.add_row(row![ - "socket address", - format!("{:?}", http_listener.address) - ]); - table.add_row(row![ - "public address", - format!("{:?}", http_listener.public_address), - ]); - table.add_row(row!["404", http_listener.answer_404]); - table.add_row(row!["503", http_listener.answer_503]); - table.add_row(row!["expect proxy", http_listener.expect_proxy]); - table.add_row(row!["sticky name", http_listener.sticky_name]); - table.add_row(row!["front timeout", http_listener.front_timeout]); - table.add_row(row!["back timeout", http_listener.back_timeout]); - table.add_row(row!["connect timeout", http_listener.connect_timeout]); - table.add_row(row!["request timeout", http_listener.request_timeout]); - table.add_row(row!["activated", http_listener.active]); - table.printstd(); - } - - println!("\nHTTPS LISTENERS\n================"); - - for (_, https_listener) in listeners_list.https_listeners.iter() { - let mut table = Table::new(); - table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); - let mut tls_versions = String::new(); - for tls_version in https_listener.versions.iter() { - tls_versions.push_str(&format!("{tls_version:?}\n")); - } - - table.add_row(row![ - "socket address", - format!("{:?}", https_listener.address) - ]); - table.add_row(row![ - "public address", - format!("{:?}", https_listener.public_address) - ]); - table.add_row(row!["404", https_listener.answer_404,]); - table.add_row(row!["503", https_listener.answer_503,]); - table.add_row(row!["versions", tls_versions]); - table.add_row(row![ - "cipher list", - list_string_vec(&https_listener.cipher_list), - ]); - table.add_row(row![ - "cipher suites", - list_string_vec(&https_listener.cipher_suites), - ]); - table.add_row(row![ - "signature algorithms", - list_string_vec(&https_listener.signature_algorithms), - ]); - table.add_row(row![ - "groups list", - list_string_vec(&https_listener.groups_list), - ]); - table.add_row(row!["key", format!("{:?}", https_listener.key),]); - table.add_row(row!["expect proxy", https_listener.expect_proxy,]); - table.add_row(row!["sticky name", https_listener.sticky_name,]); - table.add_row(row!["front timeout", https_listener.front_timeout,]); - table.add_row(row!["back timeout", https_listener.back_timeout,]); - table.add_row(row!["connect timeout", https_listener.connect_timeout,]); - table.add_row(row!["request timeout", https_listener.request_timeout,]); - table.add_row(row!["activated", https_listener.active]); - table.printstd(); - } - - println!("\nTCP LISTENERS\n================"); - - if !listeners_list.tcp_listeners.is_empty() { - let mut table = Table::new(); - table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); - table.add_row(row!["TCP frontends"]); - table.add_row(row![ - "socket address", - "public address", - "expect proxy", - "front timeout", - "back timeout", - "connect timeout", - "activated" - ]); - for (_, tcp_listener) in listeners_list.tcp_listeners.iter() { - table.add_row(row![ - format!("{:?}", tcp_listener.address), - format!("{:?}", tcp_listener.public_address), - tcp_listener.expect_proxy, - tcp_listener.front_timeout, - tcp_listener.back_timeout, - tcp_listener.connect_timeout, - tcp_listener.active, - ]); - } - table.printstd(); - } - Ok(()) -} - -pub fn print_status(worker_infos: WorkerInfos, json: bool) -> anyhow::Result<()> { - if json { - return print_json_response(&worker_infos); - } - let mut table = Table::new(); - table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); - table.add_row(row!["worker id", "pid", "run state"]); - - for worker_info in worker_infos.vec { - let row = row!(worker_info.id, worker_info.pid, worker_info.run_state); - table.add_row(row); - } - - table.printstd(); - Ok(()) -} - -pub fn print_frontend_list(frontends: ListedFrontends, json: bool) -> anyhow::Result<()> { - if json { - return print_json_response(&frontends); - } - trace!(" We received this frontends to display {:#?}", frontends); - // HTTP frontends - if !frontends.http_frontends.is_empty() { - let mut table = Table::new(); - table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); - table.add_row(row!["HTTP frontends "]); - table.add_row(row![ - "cluster_id", - "address", - "hostname", - "path", - "method", - "position", - "tags" - ]); - for http_frontend in frontends.http_frontends.iter() { - table.add_row(row!( - http_frontend - .cluster_id - .clone() - .unwrap_or("Deny".to_owned()), - http_frontend.address.to_string(), - http_frontend.hostname.to_string(), - format!("{:?}", http_frontend.path), - format!("{:?}", http_frontend.method), - format!("{:?}", http_frontend.position), - format_tags_to_string(&http_frontend.tags) - )); - } - table.printstd(); - } - - // HTTPS frontends - if !frontends.https_frontends.is_empty() { - let mut table = Table::new(); - table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); - table.add_row(row!["HTTPS frontends"]); - table.add_row(row![ - "cluster_id", - "address", - "hostname", - "path", - "method", - "position", - "tags" - ]); - for https_frontend in frontends.https_frontends.iter() { - table.add_row(row!( - https_frontend - .cluster_id - .clone() - .unwrap_or("Deny".to_owned()), - https_frontend.address.to_string(), - https_frontend.hostname.to_string(), - format!("{:?}", https_frontend.path), - format!("{:?}", https_frontend.method), - format!("{:?}", https_frontend.position), - format_tags_to_string(&https_frontend.tags) - )); - } - table.printstd(); - } - - // TCP frontends - if !frontends.tcp_frontends.is_empty() { - let mut table = Table::new(); - table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); - table.add_row(row!["TCP frontends "]); - table.add_row(row!["Cluster ID", "address", "tags"]); - for tcp_frontend in frontends.tcp_frontends.iter() { - table.add_row(row!( - tcp_frontend.cluster_id, - tcp_frontend.address, - format_tags_to_string(&tcp_frontend.tags) - )); - } - table.printstd(); - } - Ok(()) -} - -pub fn print_metrics( - // main & worker metrics - aggregated_metrics: AggregatedMetrics, - json: bool, -) -> anyhow::Result<()> { - if json { - debug!("Here are the metrics, per worker"); - return print_json_response(&aggregated_metrics); - } - - // main process metrics - println!("\nMAIN PROCESS\n============"); - print_proxy_metrics(&aggregated_metrics.main); - - // workers - for (worker_id, worker_metrics) in aggregated_metrics.workers.iter() { - println!("\nWorker {worker_id}\n========="); - print_worker_metrics(worker_metrics)?; - } - Ok(()) -} - -fn print_worker_metrics(worker_metrics: &WorkerMetrics) -> anyhow::Result<()> { - print_proxy_metrics(&worker_metrics.proxy); - print_cluster_metrics(&worker_metrics.clusters); - - Ok(()) -} - -fn print_proxy_metrics(proxy_metrics: &BTreeMap) { - let filtered = filter_metrics(proxy_metrics); - print_gauges_and_counts(&filtered); - print_percentiles(&filtered); -} - -fn print_cluster_metrics(cluster_metrics: &BTreeMap) { - for (cluster_id, cluster_metrics_data) in cluster_metrics.iter() { - println!("\nCluster {cluster_id}\n--------"); - - let filtered = filter_metrics(&cluster_metrics_data.cluster); - print_gauges_and_counts(&filtered); - print_percentiles(&filtered); - - for backend_metrics in cluster_metrics_data.backends.iter() { - println!("\n{cluster_id}/{}\n--------", backend_metrics.backend_id); - let filtered = filter_metrics(&backend_metrics.metrics); - print_gauges_and_counts(&filtered); - print_percentiles(&filtered); - } - } -} - -fn filter_metrics( - metrics: &BTreeMap, -) -> BTreeMap { - let mut filtered_metrics = BTreeMap::new(); - - for (metric_key, filtered_value) in metrics.iter() { - filtered_metrics.insert( - metric_key.replace('\t', ".").to_string(), - filtered_value.clone(), - ); - } - filtered_metrics -} - -fn print_gauges_and_counts(filtered_metrics: &BTreeMap) { - let mut titles: Vec = filtered_metrics - .iter() - .filter_map(|(title, filtered_data)| match filtered_data.inner { - Some(filtered_metrics::Inner::Count(_)) | Some(filtered_metrics::Inner::Gauge(_)) => { - Some(title.to_owned()) - } - _ => None, - }) - .collect(); - - // sort the titles so they always appear in the same order - titles.sort(); - - if titles.is_empty() { - return; - } - - let mut table = Table::new(); - table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); - - table.set_titles(Row::new(vec![cell!(""), cell!("gauge"), cell!("count")])); - - for title in titles { - let mut row = vec![cell!(title)]; - match filtered_metrics.get(&title) { - Some(filtered_metrics) => match filtered_metrics.inner { - Some(filtered_metrics::Inner::Count(c)) => { - row.push(cell!("")); - row.push(cell!(c)) - } - Some(filtered_metrics::Inner::Gauge(c)) => { - row.push(cell!(c)); - row.push(cell!("")) - } - _ => {} - }, - _ => row.push(cell!("")), - } - table.add_row(Row::new(row)); - } - - table.printstd(); -} - -fn print_percentiles(filtered_metrics: &BTreeMap) { - let mut percentile_titles: Vec = filtered_metrics - .iter() - .filter_map(|(title, filtered_data)| match filtered_data.inner.clone() { - Some(filtered_metrics::Inner::Percentiles(_)) => Some(title.to_owned()), - _ => None, - }) - .collect(); - - // sort the metrics so they always appear in the same order - percentile_titles.sort(); - - if percentile_titles.is_empty() { - return; - } - - let mut percentile_table = Table::new(); - percentile_table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); - - percentile_table.set_titles(Row::new(vec![ - cell!("Percentiles"), - cell!("samples"), - cell!("p50"), - cell!("p90"), - cell!("p99"), - cell!("p99.9"), - cell!("p99.99"), - cell!("p99.999"), - cell!("p100"), - ])); - - for title in percentile_titles { - if let Some(FilteredMetrics { - inner: Some(filtered_metrics::Inner::Percentiles(percentiles)), - }) = filtered_metrics.get(&title) - { - percentile_table.add_row(Row::new(vec![ - cell!(title), - cell!(percentiles.samples), - cell!(percentiles.p_50), - cell!(percentiles.p_90), - cell!(percentiles.p_99), - cell!(percentiles.p_99_9), - cell!(percentiles.p_99_99), - cell!(percentiles.p_99_999), - cell!(percentiles.p_100), - ])); - } else { - println!("Something went VERY wrong here"); - } - } - - percentile_table.printstd(); -} - -pub fn print_json_response(input: &T) -> Result<(), anyhow::Error> { - println!( - "{}", - serde_json::to_string_pretty(&input).context("Error while parsing response to JSON")? - ); - Ok(()) -} - -/// Creates an empty table of the form -/// ```text -/// ┌────────────┬─────────────┬───────────┬────────┐ -/// │ │ header │ header │ header │ -/// ├────────────┼─────────────┼───────────┼────────┤ -/// │ cluster_id │ │ │ │ -/// ├────────────┼─────────────┼───────────┼────────┤ -/// │ cluster_id │ │ │ │ -/// ├────────────┼─────────────┼───────────┼────────┤ -/// │ cluster_id │ │ │ │ -/// └────────────┴─────────────┴───────────┴────────┘ -/// ``` -pub fn create_cluster_table(headers: Vec<&str>, data: &BTreeMap) -> Table { - let mut table = Table::new(); - table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); - let mut row_header: Vec<_> = headers.iter().map(|h| cell!(h)).collect(); - for ref key in data.keys() { - row_header.push(cell!(&key)); - } - table.add_row(Row::new(row_header)); - table -} - -pub fn print_cluster_responses( - cluster_id: Option, - domain: Option, - worker_responses: WorkerResponses, - json: bool, -) -> anyhow::Result<()> { - if json { - return print_json_response(&worker_responses); - } - - if let Some(needle) = cluster_id.or(domain) { - let mut cluster_table = create_cluster_table( - vec!["id", "sticky_session", "https_redirect"], - &worker_responses.map, - ); - - let mut frontend_table = - create_cluster_table(vec!["id", "hostname", "path"], &worker_responses.map); - - let mut https_frontend_table = - create_cluster_table(vec!["id", "hostname", "path"], &worker_responses.map); - - let mut tcp_frontend_table = - create_cluster_table(vec!["id", "address"], &worker_responses.map); - - let mut backend_table = create_cluster_table( - vec!["backend id", "IP address", "Backup"], - &worker_responses.map, - ); - - let worker_ids: HashSet<&String> = worker_responses.map.keys().collect(); - - let mut cluster_infos = HashMap::new(); - let mut http_frontends = HashMap::new(); - let mut https_frontends = HashMap::new(); - let mut tcp_frontends = HashMap::new(); - let mut backends = HashMap::new(); - - for (worker_id, response_content) in worker_responses.map.iter() { - if let Some(ContentType::Clusters(clusters)) = &response_content.content_type { - for cluster in clusters.vec.iter() { - if cluster.configuration.is_some() { - let entry = cluster_infos.entry(cluster).or_insert(Vec::new()); - entry.push(worker_id.to_owned()); - } - - for frontend in cluster.http_frontends.iter() { - let entry = http_frontends.entry(frontend).or_insert(Vec::new()); - entry.push(worker_id.to_owned()); - } - - for frontend in cluster.https_frontends.iter() { - let entry = https_frontends.entry(frontend).or_insert(Vec::new()); - entry.push(worker_id.to_owned()); - } - - for frontend in cluster.tcp_frontends.iter() { - let entry = tcp_frontends.entry(frontend).or_insert(Vec::new()); - entry.push(worker_id.to_owned()); - } - - for backend in cluster.backends.iter() { - let entry = backends.entry(backend).or_insert(Vec::new()); - entry.push(worker_id.to_owned()); - } - } - } - } - - println!("Cluster level configuration for {needle}:\n"); - - for (cluster_info, workers_the_cluster_is_present_on) in cluster_infos.iter() { - let mut row = Vec::new(); - row.push(cell!(cluster_info - .configuration - .as_ref() - .map(|conf| conf.cluster_id.to_owned()) - .unwrap_or_else(|| String::from("None")))); - row.push(cell!(cluster_info - .configuration - .as_ref() - .map(|conf| conf.sticky_session) - .unwrap_or_else(|| false))); - row.push(cell!(cluster_info - .configuration - .as_ref() - .map(|conf| conf.https_redirect) - .unwrap_or_else(|| false))); - - for worker in workers_the_cluster_is_present_on { - if worker_ids.contains(worker) { - row.push(cell!("X")); - } else { - row.push(cell!("")); - } - } - - cluster_table.add_row(Row::new(row)); - } - - cluster_table.printstd(); - - println!("\nHTTP frontends configuration for {needle}:\n"); - - for (key, values) in http_frontends.iter() { - let mut row = Vec::new(); - match &key.cluster_id { - Some(cluster_id) => row.push(cell!(cluster_id)), - None => row.push(cell!("-")), - } - row.push(cell!(key.hostname)); - row.push(cell!(key.path)); - - for val in values.iter() { - if worker_ids.contains(val) { - row.push(cell!("X")); - } else { - row.push(cell!("")); - } - } - - frontend_table.add_row(Row::new(row)); - } - - frontend_table.printstd(); - - println!("\nHTTPS frontends configuration for {needle}:\n"); - - for (key, values) in https_frontends.iter() { - let mut row = Vec::new(); - match &key.cluster_id { - Some(cluster_id) => row.push(cell!(cluster_id)), - None => row.push(cell!("-")), - } - row.push(cell!(key.hostname)); - row.push(cell!(key.path)); - - for val in values.iter() { - if worker_ids.contains(val) { - row.push(cell!("X")); - } else { - row.push(cell!("")); - } - } - - https_frontend_table.add_row(Row::new(row)); - } - - https_frontend_table.printstd(); - - println!("\nTCP frontends configuration for {needle}:\n"); - - for (key, values) in tcp_frontends.iter() { - let mut row = vec![cell!(key.cluster_id), cell!(format!("{}", key.address))]; - - for val in values.iter() { - if worker_ids.contains(val) { - row.push(cell!(String::from("X"))); - } else { - row.push(cell!(String::from(""))); - } - } - - tcp_frontend_table.add_row(Row::new(row)); - } - - tcp_frontend_table.printstd(); - - println!("\nbackends configuration for {needle}:\n"); - - for (key, values) in backends.iter() { - let mut row = vec![ - cell!(key.backend_id), - cell!(format!("{}", key.address)), - cell!(key - .backup - .map(|b| if b { "X" } else { "" }) - .unwrap_or_else(|| "")), - ]; - - for val in values { - if worker_ids.contains(&val) { - row.push(cell!("X")); - } else { - row.push(cell!("")); - } - } - - backend_table.add_row(Row::new(row)); - } - - backend_table.printstd(); - - return Ok(()); - } - - // display all clusters in a simplified table showing their hashes - let mut clusters_table = Table::new(); - clusters_table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); - let mut header = vec![cell!("cluster id")]; - for worker_id in worker_responses.map.keys() { - header.push(cell!(format!("worker {}", worker_id))); - } - header.push(cell!("desynchronized")); - clusters_table.add_row(Row::new(header)); - - let mut cluster_hashes = HashMap::new(); - - for response_content in worker_responses.map.values() { - if let Some(ContentType::ClusterHashes(hashes)) = &response_content.content_type { - for (cluster_id, hash) in hashes.map.iter() { - cluster_hashes - .entry(cluster_id) - .or_insert(Vec::new()) - .push(hash); - } - } - } - - for (cluster_id, hashes) in cluster_hashes.iter() { - let mut row = vec![cell!(cluster_id)]; - for val in hashes.iter() { - row.push(cell!(format!("{val}"))); - } - - let hs: HashSet<&u64> = hashes.iter().cloned().collect(); - if hs.len() > 1 { - row.push(cell!("X")); - } else { - row.push(cell!("")); - } - - clusters_table.add_row(Row::new(row)); - } - - clusters_table.printstd(); - Ok(()) -} - -pub fn print_certificates_by_worker( - response_contents: BTreeMap, - json: bool, -) -> anyhow::Result<()> { - if json { - return print_json_response(&response_contents); - } - - for (worker_id, response_content) in response_contents.iter() { - println!("Worker {}", worker_id); - match &response_content.content_type { - Some(ContentType::CertificatesByAddress(list)) => { - for certs in list.certificates.iter() { - println!("\t{}:", certs.address); - - for summary in certs.certificate_summaries.iter() { - println!("\t\t{}", summary); - } - - println!(); - } - } - Some(ContentType::CertificatesWithFingerprints(CertificatesWithFingerprints { - certs, - })) => print_certificates_with_validity(certs.clone())?, - - _ => {} - } - println!(); - } - Ok(()) -} - -fn format_tags_to_string(tags: &BTreeMap) -> String { - tags.iter() - .map(|(k, v)| format!("{k}={v}")) - .collect::>() - .join(", ") -} - -pub fn print_available_metrics( - available_metrics: &AvailableMetrics, - json: bool, -) -> anyhow::Result<()> { - if json { - return print_json_response(&available_metrics); - } - println!("Available metrics on the proxy level:"); - for metric_name in &available_metrics.proxy_metrics { - println!("\t{metric_name}"); - } - println!("Available metrics on the cluster level:"); - for metric_name in &available_metrics.cluster_metrics { - println!("\t{metric_name}"); - } - Ok(()) -} - -fn list_string_vec(vec: &[String]) -> String { - let mut output = String::new(); - for item in vec.iter() { - output.push_str(item); - output.push('\n'); - } - output -} - -pub fn print_certificates_with_validity( - certs: BTreeMap, -) -> anyhow::Result<()> { - let mut table = Table::new(); - table.set_format(*prettytable::format::consts::FORMAT_CLEAN); - table.add_row(row![ - "fingeprint", - "valid not before", - "valide not after", - "domain names", - ]); - - for (fingerprint, cert) in certs { - let (_unparsed, pem_certificate) = - x509_parser::pem::parse_x509_pem(cert.certificate.as_bytes()) - .with_context(|| "Could not parse pem certificate")?; - - let x509_certificate = pem_certificate - .parse_x509() - .with_context(|| "Could not parse x509 certificate")?; - - let validity = x509_certificate.validity(); - - table.add_row(row!( - fingerprint, - format_datetime(validity.not_before)?, - format_datetime(validity.not_after)?, - concatenate_vector(&cert.names), - )); - } - table.printstd(); - - Ok(()) -} - -pub fn print_request_counts(request_counts: &RequestCounts, json: bool) -> anyhow::Result<()> { - if json { - return print_json_response(&request_counts); - } - let mut table = Table::new(); - table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); - table.add_row(row!["request type", "count"]); - - for (request_type, count) in &request_counts.map { - table.add_row(row!(request_type, count)); - } - table.printstd(); - Ok(()) -} - -// ISO 8601 -fn format_datetime(asn1_time: ASN1Time) -> anyhow::Result { - let datetime = asn1_time.to_datetime(); - - let formatted = datetime - .format(&format_description::well_known::Iso8601::DEFAULT) - .with_context(|| "Could not format the datetime to ISO 8601")?; - Ok(formatted) -} diff --git a/bin/src/ctl/mod.rs b/bin/src/ctl/mod.rs index ceeb00862..e8b23f4f1 100644 --- a/bin/src/ctl/mod.rs +++ b/bin/src/ctl/mod.rs @@ -14,8 +14,6 @@ use crate::{ }; mod command; -/// TODO: just create a display() method on sozu_command_lib::Response and put everything in there -mod display; mod request_builder; pub struct CommandManager { diff --git a/bin/src/ctl/request_builder.rs b/bin/src/ctl/request_builder.rs index da0151d43..ce0ff3ec1 100644 --- a/bin/src/ctl/request_builder.rs +++ b/bin/src/ctl/request_builder.rs @@ -3,13 +3,16 @@ use std::collections::BTreeMap; use anyhow::{bail, Context}; use sozu_command_lib::{ - certificate::{calculate_fingerprint, split_certificate_chain, Fingerprint}, - config::{Config, ListenerBuilder}, + certificate::{ + decode_fingerprint, get_fingerprint_from_certificate_path, load_full_certificate, + }, + config::ListenerBuilder, proto::command::{ - request::RequestType, ActivateListener, AddBackend, AddCertificate, CertificateAndKey, - Cluster, CountRequests, DeactivateListener, FrontendFilters, HardStop, ListListeners, - ListenerType, LoadBalancingParams, MetricsConfiguration, PathRule, ProxyProtocolConfig, - RemoveBackend, RemoveCertificate, RemoveListener, ReplaceCertificate, RequestHttpFrontend, + request::RequestType, ActivateListener, AddBackend, AddCertificate, Cluster, CountRequests, + DeactivateListener, FrontendFilters, HardStop, ListListeners, ListenerType, + LoadBalancingParams, MetricsConfiguration, PathRule, ProxyProtocolConfig, + QueryCertificatesFilters, QueryClusterByDomain, QueryClustersHashes, RemoveBackend, + RemoveCertificate, RemoveListener, ReplaceCertificate, RequestHttpFrontend, RequestTcpFrontend, RulePosition, SoftStop, Status, SubscribeEvents, TlsVersion, }, }; @@ -166,7 +169,39 @@ impl CommandManager { ) } ClusterCmd::Remove { id } => self.send_request(RequestType::RemoveCluster(id).into()), - ClusterCmd::List { id, domain } => self.query_cluster(id, domain), + ClusterCmd::List { + id: cluster_id, + domain, + } => { + if cluster_id.is_some() && domain.is_some() { + bail!("Error: Either request an cluster ID or a domain name"); + } + + let request = if let Some(ref cluster_id) = cluster_id { + RequestType::QueryClusterById(cluster_id.to_string()).into() + } else if let Some(ref domain) = domain { + let splitted: Vec = + domain.splitn(2, '/').map(|elem| elem.to_string()).collect(); + + if splitted.is_empty() { + bail!("Domain can't be empty"); + } + + let query_domain = QueryClusterByDomain { + hostname: splitted + .get(0) + .with_context(|| "Domain can't be empty")? + .clone(), + path: splitted.get(1).cloned().map(|path| format!("/{path}")), // We add the / again because of the splitn removing it + }; + + RequestType::QueryClustersByDomain(query_domain).into() + } else { + RequestType::QueryClustersHashes(QueryClustersHashes {}).into() + }; + + self.send_request(request) + } } } @@ -551,51 +586,22 @@ impl CommandManager { .into(), ) } -} - -fn get_fingerprint_from_certificate_path(certificate_path: &str) -> anyhow::Result { - let bytes = Config::load_file_bytes(certificate_path) - .with_context(|| format!("could not load certificate file on path {certificate_path}"))?; - - let parsed_bytes = calculate_fingerprint(&bytes).with_context(|| { - format!("could not calculate fingerprint for the certificate at {certificate_path}") - })?; - Ok(Fingerprint(parsed_bytes)) -} - -fn decode_fingerprint(fingerprint: &str) -> anyhow::Result { - let bytes = hex::decode(fingerprint) - .with_context(|| "Failed at decoding the string (expected hexadecimal data)")?; - Ok(Fingerprint(bytes)) -} + pub fn query_certificates( + &mut self, + fingerprint: Option, + domain: Option, + query_workers: bool, + ) -> Result<(), anyhow::Error> { + let filters = QueryCertificatesFilters { + domain, + fingerprint, + }; -fn load_full_certificate( - certificate_path: &str, - certificate_chain_path: &str, - key_path: &str, - versions: Vec, - names: Vec, -) -> Result { - let certificate = Config::load_file(certificate_path) - .with_context(|| format!("Could not load certificate file on path {certificate_path}"))?; - - let certificate_chain = Config::load_file(certificate_chain_path) - .map(split_certificate_chain) - .with_context(|| { - format!("could not load certificate chain on path: {certificate_chain_path}") - })?; - - let key = Config::load_file(key_path) - .with_context(|| format!("Could not load key file on path {key_path}"))?; - - let versions = versions.iter().map(|v| *v as i32).collect(); - - Ok(CertificateAndKey { - certificate, - certificate_chain, - key, - versions, - names, - }) + if query_workers { + self.send_request(RequestType::QueryCertificatesFromWorkers(filters).into()) + } else { + self.send_request(RequestType::QueryCertificatesFromTheState(filters).into()) + } + } } diff --git a/bin/src/main.rs b/bin/src/main.rs index d1ec36990..77969aa0b 100644 --- a/bin/src/main.rs +++ b/bin/src/main.rs @@ -23,8 +23,6 @@ //! which means other programs can use the protobuf definition and send roquests //! to Sōzu via its UNIX socket. -#[macro_use] -extern crate prettytable; #[macro_use] extern crate sozu_lib as sozu; #[macro_use] diff --git a/command/Cargo.toml b/command/Cargo.toml index f1939a800..fa3bfef80 100644 --- a/command/Cargo.toml +++ b/command/Cargo.toml @@ -42,6 +42,7 @@ serde = { version = "^1.0.188", features = ["derive"] } serde_json = "^1.0.107" sha2 = "^0.10.8" trailer = "^0.1.2" +prettytable-rs = { version = "^0.10.0", default-features = false } pool = "^0.1.4" poule = "^0.3.2" thiserror = "^1.0.49" diff --git a/command/src/certificate.rs b/command/src/certificate.rs index 517ebc63b..cc27f7808 100644 --- a/command/src/certificate.rs +++ b/command/src/certificate.rs @@ -11,12 +11,15 @@ use x509_parser::{ pem::{parse_x509_pem, Pem}, }; -use crate::proto::command::TlsVersion; +use crate::{ + config::{Config, ConfigError}, + proto::command::{CertificateAndKey, TlsVersion}, +}; // ----------------------------------------------------------------------------- // CertificateError -#[derive(thiserror::Error, Clone, Debug)] +#[derive(thiserror::Error, Debug)] pub enum CertificateError { #[error("Could not parse PEM certificate from bytes: {0}")] InvalidCertificate(String), @@ -24,6 +27,10 @@ pub enum CertificateError { InvalidTlsVersion(String), #[error("failed to parse fingerprint, {0}")] InvalidFingerprint(FromHexError), + #[error("could not load file on path {path}: {error}")] + LoadFile { path: String, error: ConfigError }, + #[error("Failed at decoding the hex encoded certificate: {0}")] + DecodeError(FromHexError), } // ----------------------------------------------------------------------------- @@ -191,3 +198,59 @@ pub fn split_certificate_chain(mut chain: String) -> Vec { v } + +pub fn get_fingerprint_from_certificate_path( + certificate_path: &str, +) -> Result { + let bytes = + Config::load_file_bytes(certificate_path).map_err(|e| CertificateError::LoadFile { + path: certificate_path.to_string(), + error: e, + })?; + + let parsed_bytes = calculate_fingerprint(&bytes)?; + + Ok(Fingerprint(parsed_bytes)) +} + +pub fn decode_fingerprint(fingerprint: &str) -> Result { + let bytes = + hex::decode(fingerprint).map_err(|hex_error| CertificateError::DecodeError(hex_error))?; + Ok(Fingerprint(bytes)) +} + +pub fn load_full_certificate( + certificate_path: &str, + certificate_chain_path: &str, + key_path: &str, + versions: Vec, + names: Vec, +) -> Result { + let certificate = + Config::load_file(certificate_path).map_err(|e| CertificateError::LoadFile { + path: certificate_path.to_string(), + error: e, + })?; + + let certificate_chain = Config::load_file(certificate_chain_path) + .map(split_certificate_chain) + .map_err(|e| CertificateError::LoadFile { + path: certificate_chain_path.to_string(), + error: e, + })?; + + let key = Config::load_file(key_path).map_err(|e| CertificateError::LoadFile { + path: key_path.to_string(), + error: e, + })?; + + let versions = versions.iter().map(|v| *v as i32).collect(); + + Ok(CertificateAndKey { + certificate, + certificate_chain, + key, + versions, + names, + }) +} diff --git a/command/src/command.proto b/command/src/command.proto index 408bde6e0..f69db483d 100644 --- a/command/src/command.proto +++ b/command/src/command.proto @@ -467,6 +467,7 @@ message ResponseContent { } } +// a map of worker_id -> ResponseContent message WorkerResponses { map map = 1; } diff --git a/command/src/proto/display.rs b/command/src/proto/display.rs index fd2e2dad7..3b7f02b5c 100644 --- a/command/src/proto/display.rs +++ b/command/src/proto/display.rs @@ -1,8 +1,21 @@ -use std::fmt::{Display, Formatter}; +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + fmt::{Display, Formatter}, +}; + +use prettytable::{cell, row, Row, Table}; +use time::format_description; +use x509_parser::time::ASN1Time; -use crate::proto::command::{ - request::RequestType, CertificateAndKey, CertificateSummary, QueryCertificatesFilters, - TlsVersion, +use crate::proto::{ + command::{ + filtered_metrics, request::RequestType, response_content::ContentType, AggregatedMetrics, + AvailableMetrics, CertificateAndKey, CertificateSummary, CertificatesWithFingerprints, + ClusterMetrics, FilteredMetrics, ListOfCertificatesByAddress, ListedFrontends, + ListenersList, QueryCertificatesFilters, RequestCounts, Response, ResponseContent, + ResponseStatus, RunState, TlsVersion, WorkerInfos, WorkerMetrics, WorkerResponses, + }, + DisplayError, }; impl Display for CertificateAndKey { @@ -91,3 +104,821 @@ pub fn format_request_type(request_type: &RequestType) -> String { RequestType::QueryCertificatesFromWorkers(_) => "QueryCertificatesFromWorkers".to_owned(), } } + +pub fn print_json_response(input: &T) -> Result<(), DisplayError> { + let pretty_json = serde_json::to_string_pretty(&input).map_err(DisplayError::Json)?; + println!("{pretty_json}"); + Ok(()) +} + +impl Response { + pub fn display(&self, json: bool) -> Result<(), DisplayError> { + match self.status() { + ResponseStatus::Ok => println!("Success: {}", self.message), + ResponseStatus::Failure => println!("Failure: {}", self.message), + ResponseStatus::Processing => { + return Err(DisplayError::WrongResponseType( + "ResponseStatus::Processing".to_string(), + )) + } + } + + let content = match &self.content { + Some(content) => content, + None => return Ok(println!("No content")), + }; + + content.display(json) + } +} + +impl ResponseContent { + fn display(&self, json: bool) -> Result<(), DisplayError> { + let content_type = match &self.content_type { + Some(content_type) => content_type, + None => return Ok(println!("No content")), + }; + + if json { + return print_json_response(&content_type); + } + + match content_type { + ContentType::Workers(worker_infos) => print_status(worker_infos), + ContentType::Metrics(aggr_metrics) => print_metrics(aggr_metrics), + ContentType::FrontendList(frontends) => print_frontends(frontends), + ContentType::ListenersList(listeners) => print_listeners(listeners), + ContentType::WorkerMetrics(worker_metrics) => print_worker_metrics(&worker_metrics), + ContentType::AvailableMetrics(list) => print_available_metrics(&list), + ContentType::RequestCounts(request_counts) => print_request_counts(&request_counts), + ContentType::CertificatesWithFingerprints(certs) => { + print_certificates_with_validity(certs) + } + ContentType::WorkerResponses(worker_responses) => { + // exception when displaying clusters + if worker_responses.contain_cluster_infos() { + print_cluster_infos(worker_responses) + } else if worker_responses.contain_cluster_hashes() { + print_cluster_hashes(worker_responses) + } else { + print_responses_by_worker(worker_responses, json) + } + } + ContentType::Clusters(_) | ContentType::ClusterHashes(_) => Ok(()), // not displayed directly, see print_cluster_responses + ContentType::CertificatesByAddress(certs) => print_certificates_by_address(certs), + ContentType::Event(_event) => Ok(()), // not event displayed yet! + } + } +} + +impl WorkerResponses { + fn contain_cluster_infos(&self) -> bool { + for (_worker_id, response) in self.map.iter() { + if let Some(content_type) = &response.content_type { + if matches!(content_type, ContentType::Clusters(_)) { + return true; + } + } + } + false + } + + fn contain_cluster_hashes(&self) -> bool { + for (_worker_id, response) in self.map.iter() { + if let Some(content_type) = &response.content_type { + if matches!(content_type, ContentType::ClusterHashes(_)) { + return true; + } + } + } + false + } +} + +pub fn print_status(worker_infos: &WorkerInfos) -> Result<(), DisplayError> { + let mut table = Table::new(); + table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); + table.add_row(row!["worker id", "pid", "run state"]); + + for worker_info in &worker_infos.vec { + let row = row!( + worker_info.id, + worker_info.pid, + RunState::try_from(worker_info.run_state) + .map_err(|e| DisplayError::DecodeError(e))? + .as_str_name() + ); + table.add_row(row); + } + + table.printstd(); + Ok(()) +} + +pub fn print_metrics(aggregated_metrics: &AggregatedMetrics) -> Result<(), DisplayError> { + // main process metrics + println!("\nMAIN PROCESS\n============"); + print_proxy_metrics(&aggregated_metrics.main); + + // workers + for (worker_id, worker_metrics) in aggregated_metrics.workers.iter() { + println!("\nWorker {worker_id}\n========="); + print_worker_metrics(worker_metrics)?; + } + Ok(()) +} + +fn print_proxy_metrics(proxy_metrics: &BTreeMap) { + let filtered = filter_metrics(proxy_metrics); + print_gauges_and_counts(&filtered); + print_percentiles(&filtered); +} + +fn print_worker_metrics(worker_metrics: &WorkerMetrics) -> Result<(), DisplayError> { + print_proxy_metrics(&worker_metrics.proxy); + print_cluster_metrics(&worker_metrics.clusters); + + Ok(()) +} + +fn print_cluster_metrics(cluster_metrics: &BTreeMap) { + for (cluster_id, cluster_metrics_data) in cluster_metrics.iter() { + println!("\nCluster {cluster_id}\n--------"); + + let filtered = filter_metrics(&cluster_metrics_data.cluster); + print_gauges_and_counts(&filtered); + print_percentiles(&filtered); + + for backend_metrics in cluster_metrics_data.backends.iter() { + println!("\n{cluster_id}/{}\n--------", backend_metrics.backend_id); + let filtered = filter_metrics(&backend_metrics.metrics); + print_gauges_and_counts(&filtered); + print_percentiles(&filtered); + } + } +} + +fn filter_metrics( + metrics: &BTreeMap, +) -> BTreeMap { + let mut filtered_metrics = BTreeMap::new(); + + for (metric_key, filtered_value) in metrics.iter() { + filtered_metrics.insert( + metric_key.replace('\t', ".").to_string(), + filtered_value.clone(), + ); + } + filtered_metrics +} + +fn print_gauges_and_counts(filtered_metrics: &BTreeMap) { + let mut titles: Vec = filtered_metrics + .iter() + .filter_map(|(title, filtered_data)| match filtered_data.inner { + Some(filtered_metrics::Inner::Count(_)) | Some(filtered_metrics::Inner::Gauge(_)) => { + Some(title.to_owned()) + } + _ => None, + }) + .collect(); + + // sort the titles so they always appear in the same order + titles.sort(); + + if titles.is_empty() { + return; + } + + let mut table = Table::new(); + table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); + + table.set_titles(Row::new(vec![cell!(""), cell!("gauge"), cell!("count")])); + + for title in titles { + let mut row = vec![cell!(title)]; + match filtered_metrics.get(&title) { + Some(filtered_metrics) => match filtered_metrics.inner { + Some(filtered_metrics::Inner::Count(c)) => { + row.push(cell!("")); + row.push(cell!(c)) + } + Some(filtered_metrics::Inner::Gauge(c)) => { + row.push(cell!(c)); + row.push(cell!("")) + } + _ => {} + }, + _ => row.push(cell!("")), + } + table.add_row(Row::new(row)); + } + + table.printstd(); +} + +fn print_percentiles(filtered_metrics: &BTreeMap) { + let mut percentile_titles: Vec = filtered_metrics + .iter() + .filter_map(|(title, filtered_data)| match filtered_data.inner.clone() { + Some(filtered_metrics::Inner::Percentiles(_)) => Some(title.to_owned()), + _ => None, + }) + .collect(); + + // sort the metrics so they always appear in the same order + percentile_titles.sort(); + + if percentile_titles.is_empty() { + return; + } + + let mut percentile_table = Table::new(); + percentile_table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); + + percentile_table.set_titles(Row::new(vec![ + cell!("Percentiles"), + cell!("samples"), + cell!("p50"), + cell!("p90"), + cell!("p99"), + cell!("p99.9"), + cell!("p99.99"), + cell!("p99.999"), + cell!("p100"), + ])); + + for title in percentile_titles { + if let Some(FilteredMetrics { + inner: Some(filtered_metrics::Inner::Percentiles(percentiles)), + }) = filtered_metrics.get(&title) + { + percentile_table.add_row(Row::new(vec![ + cell!(title), + cell!(percentiles.samples), + cell!(percentiles.p_50), + cell!(percentiles.p_90), + cell!(percentiles.p_99), + cell!(percentiles.p_99_9), + cell!(percentiles.p_99_99), + cell!(percentiles.p_99_999), + cell!(percentiles.p_100), + ])); + } else { + println!("Something went VERY wrong here"); + } + } + + percentile_table.printstd(); +} + +fn print_available_metrics(available_metrics: &AvailableMetrics) -> Result<(), DisplayError> { + println!("Available metrics on the proxy level:"); + for metric_name in &available_metrics.proxy_metrics { + println!("\t{metric_name}"); + } + println!("Available metrics on the cluster level:"); + for metric_name in &available_metrics.cluster_metrics { + println!("\t{metric_name}"); + } + Ok(()) +} + +fn print_frontends(frontends: &ListedFrontends) -> Result<(), DisplayError> { + trace!(" We received this frontends to display {:#?}", frontends); + // HTTP frontends + if !frontends.http_frontends.is_empty() { + let mut table = Table::new(); + table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); + table.add_row(row!["HTTP frontends "]); + table.add_row(row![ + "cluster_id", + "address", + "hostname", + "path", + "method", + "position", + "tags" + ]); + for http_frontend in frontends.http_frontends.iter() { + table.add_row(row!( + http_frontend + .cluster_id + .clone() + .unwrap_or("Deny".to_owned()), + http_frontend.address.to_string(), + http_frontend.hostname.to_string(), + format!("{:?}", http_frontend.path), + format!("{:?}", http_frontend.method), + format!("{:?}", http_frontend.position), + format_tags_to_string(&http_frontend.tags) + )); + } + table.printstd(); + } + + // HTTPS frontends + if !frontends.https_frontends.is_empty() { + let mut table = Table::new(); + table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); + table.add_row(row!["HTTPS frontends"]); + table.add_row(row![ + "cluster_id", + "address", + "hostname", + "path", + "method", + "position", + "tags" + ]); + for https_frontend in frontends.https_frontends.iter() { + table.add_row(row!( + https_frontend + .cluster_id + .clone() + .unwrap_or("Deny".to_owned()), + https_frontend.address.to_string(), + https_frontend.hostname.to_string(), + format!("{:?}", https_frontend.path), + format!("{:?}", https_frontend.method), + format!("{:?}", https_frontend.position), + format_tags_to_string(&https_frontend.tags) + )); + } + table.printstd(); + } + + // TCP frontends + if !frontends.tcp_frontends.is_empty() { + let mut table = Table::new(); + table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); + table.add_row(row!["TCP frontends "]); + table.add_row(row!["Cluster ID", "address", "tags"]); + for tcp_frontend in frontends.tcp_frontends.iter() { + table.add_row(row!( + tcp_frontend.cluster_id, + tcp_frontend.address, + format_tags_to_string(&tcp_frontend.tags) + )); + } + table.printstd(); + } + Ok(()) +} + +pub fn print_listeners(listeners_list: &ListenersList) -> Result<(), DisplayError> { + println!("\nHTTP LISTENERS\n================"); + + for (_, http_listener) in listeners_list.http_listeners.iter() { + let mut table = Table::new(); + table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); + table.add_row(row![ + "socket address", + format!("{:?}", http_listener.address) + ]); + table.add_row(row![ + "public address", + format!("{:?}", http_listener.public_address), + ]); + table.add_row(row!["404", http_listener.answer_404]); + table.add_row(row!["503", http_listener.answer_503]); + table.add_row(row!["expect proxy", http_listener.expect_proxy]); + table.add_row(row!["sticky name", http_listener.sticky_name]); + table.add_row(row!["front timeout", http_listener.front_timeout]); + table.add_row(row!["back timeout", http_listener.back_timeout]); + table.add_row(row!["connect timeout", http_listener.connect_timeout]); + table.add_row(row!["request timeout", http_listener.request_timeout]); + table.add_row(row!["activated", http_listener.active]); + table.printstd(); + } + + println!("\nHTTPS LISTENERS\n================"); + + for (_, https_listener) in listeners_list.https_listeners.iter() { + let mut table = Table::new(); + table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); + let mut tls_versions = String::new(); + for tls_version in https_listener.versions.iter() { + tls_versions.push_str(&format!("{tls_version:?}\n")); + } + + table.add_row(row![ + "socket address", + format!("{:?}", https_listener.address) + ]); + table.add_row(row![ + "public address", + format!("{:?}", https_listener.public_address) + ]); + table.add_row(row!["404", https_listener.answer_404,]); + table.add_row(row!["503", https_listener.answer_503,]); + table.add_row(row!["versions", tls_versions]); + table.add_row(row![ + "cipher list", + list_string_vec(&https_listener.cipher_list), + ]); + table.add_row(row![ + "cipher suites", + list_string_vec(&https_listener.cipher_suites), + ]); + table.add_row(row![ + "signature algorithms", + list_string_vec(&https_listener.signature_algorithms), + ]); + table.add_row(row![ + "groups list", + list_string_vec(&https_listener.groups_list), + ]); + table.add_row(row!["key", format!("{:?}", https_listener.key),]); + table.add_row(row!["expect proxy", https_listener.expect_proxy,]); + table.add_row(row!["sticky name", https_listener.sticky_name,]); + table.add_row(row!["front timeout", https_listener.front_timeout,]); + table.add_row(row!["back timeout", https_listener.back_timeout,]); + table.add_row(row!["connect timeout", https_listener.connect_timeout,]); + table.add_row(row!["request timeout", https_listener.request_timeout,]); + table.add_row(row!["activated", https_listener.active]); + table.printstd(); + } + + println!("\nTCP LISTENERS\n================"); + + if !listeners_list.tcp_listeners.is_empty() { + let mut table = Table::new(); + table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); + table.add_row(row!["TCP frontends"]); + table.add_row(row![ + "socket address", + "public address", + "expect proxy", + "front timeout", + "back timeout", + "connect timeout", + "activated" + ]); + for (_, tcp_listener) in listeners_list.tcp_listeners.iter() { + table.add_row(row![ + format!("{:?}", tcp_listener.address), + format!("{:?}", tcp_listener.public_address), + tcp_listener.expect_proxy, + tcp_listener.front_timeout, + tcp_listener.back_timeout, + tcp_listener.connect_timeout, + tcp_listener.active, + ]); + } + table.printstd(); + } + Ok(()) +} + +fn print_cluster_infos(worker_responses: &WorkerResponses) -> Result<(), DisplayError> { + let mut cluster_table = create_cluster_table( + vec!["id", "sticky_session", "https_redirect"], + &worker_responses.map, + ); + + let mut frontend_table = + create_cluster_table(vec!["id", "hostname", "path"], &worker_responses.map); + + let mut https_frontend_table = + create_cluster_table(vec!["id", "hostname", "path"], &worker_responses.map); + + let mut tcp_frontend_table = create_cluster_table(vec!["id", "address"], &worker_responses.map); + + let mut backend_table = create_cluster_table( + vec!["backend id", "IP address", "Backup"], + &worker_responses.map, + ); + + let worker_ids: HashSet<&String> = worker_responses.map.keys().collect(); + + let mut cluster_infos = HashMap::new(); + let mut http_frontends = HashMap::new(); + let mut https_frontends = HashMap::new(); + let mut tcp_frontends = HashMap::new(); + let mut backends = HashMap::new(); + + for (worker_id, response_content) in worker_responses.map.iter() { + if let Some(ContentType::Clusters(clusters)) = &response_content.content_type { + for cluster in clusters.vec.iter() { + if cluster.configuration.is_some() { + let entry = cluster_infos.entry(cluster).or_insert(Vec::new()); + entry.push(worker_id.to_owned()); + } + + for frontend in cluster.http_frontends.iter() { + let entry = http_frontends.entry(frontend).or_insert(Vec::new()); + entry.push(worker_id.to_owned()); + } + + for frontend in cluster.https_frontends.iter() { + let entry = https_frontends.entry(frontend).or_insert(Vec::new()); + entry.push(worker_id.to_owned()); + } + + for frontend in cluster.tcp_frontends.iter() { + let entry = tcp_frontends.entry(frontend).or_insert(Vec::new()); + entry.push(worker_id.to_owned()); + } + + for backend in cluster.backends.iter() { + let entry = backends.entry(backend).or_insert(Vec::new()); + entry.push(worker_id.to_owned()); + } + } + } + } + + println!("Cluster level configuration:\n"); + + for (cluster_info, workers_the_cluster_is_present_on) in cluster_infos.iter() { + let mut row = Vec::new(); + row.push(cell!(cluster_info + .configuration + .as_ref() + .map(|conf| conf.cluster_id.to_owned()) + .unwrap_or_else(|| String::from("None")))); + row.push(cell!(cluster_info + .configuration + .as_ref() + .map(|conf| conf.sticky_session) + .unwrap_or_else(|| false))); + row.push(cell!(cluster_info + .configuration + .as_ref() + .map(|conf| conf.https_redirect) + .unwrap_or_else(|| false))); + + for worker in workers_the_cluster_is_present_on { + if worker_ids.contains(worker) { + row.push(cell!("X")); + } else { + row.push(cell!("")); + } + } + + cluster_table.add_row(Row::new(row)); + } + + cluster_table.printstd(); + + println!("\nHTTP frontends configuration for:\n"); + + for (key, values) in http_frontends.iter() { + let mut row = Vec::new(); + match &key.cluster_id { + Some(cluster_id) => row.push(cell!(cluster_id)), + None => row.push(cell!("-")), + } + row.push(cell!(key.hostname)); + row.push(cell!(key.path)); + + for val in values.iter() { + if worker_ids.contains(val) { + row.push(cell!("X")); + } else { + row.push(cell!("")); + } + } + + frontend_table.add_row(Row::new(row)); + } + + frontend_table.printstd(); + + println!("\nHTTPS frontends configuration for:\n"); + + for (key, values) in https_frontends.iter() { + let mut row = Vec::new(); + match &key.cluster_id { + Some(cluster_id) => row.push(cell!(cluster_id)), + None => row.push(cell!("-")), + } + row.push(cell!(key.hostname)); + row.push(cell!(key.path)); + + for val in values.iter() { + if worker_ids.contains(val) { + row.push(cell!("X")); + } else { + row.push(cell!("")); + } + } + + https_frontend_table.add_row(Row::new(row)); + } + + https_frontend_table.printstd(); + + println!("\nTCP frontends configuration:\n"); + + for (key, values) in tcp_frontends.iter() { + let mut row = vec![cell!(key.cluster_id), cell!(format!("{}", key.address))]; + + for val in values.iter() { + if worker_ids.contains(val) { + row.push(cell!(String::from("X"))); + } else { + row.push(cell!(String::from(""))); + } + } + + tcp_frontend_table.add_row(Row::new(row)); + } + + tcp_frontend_table.printstd(); + + println!("\nbackends configuration:\n"); + + for (key, values) in backends.iter() { + let mut row = vec![ + cell!(key.backend_id), + cell!(format!("{}", key.address)), + cell!(key + .backup + .map(|b| if b { "X" } else { "" }) + .unwrap_or_else(|| "")), + ]; + + for val in values { + if worker_ids.contains(&val) { + row.push(cell!("X")); + } else { + row.push(cell!("")); + } + } + + backend_table.add_row(Row::new(row)); + } + + backend_table.printstd(); + + Ok(()) +} + +/// display all clusters in a simplified table showing their hashes +fn print_cluster_hashes(worker_responses: &WorkerResponses) -> Result<(), DisplayError> { + let mut clusters_table = Table::new(); + clusters_table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); + let mut header = vec![cell!("cluster id")]; + for worker_id in worker_responses.map.keys() { + header.push(cell!(format!("worker {}", worker_id))); + } + header.push(cell!("desynchronized")); + clusters_table.add_row(Row::new(header)); + + let mut cluster_hashes = HashMap::new(); + + for response_content in worker_responses.map.values() { + if let Some(ContentType::ClusterHashes(hashes)) = &response_content.content_type { + for (cluster_id, hash) in hashes.map.iter() { + cluster_hashes + .entry(cluster_id) + .or_insert(Vec::new()) + .push(hash); + } + } + } + + for (cluster_id, hashes) in cluster_hashes.iter() { + let mut row = vec![cell!(cluster_id)]; + for val in hashes.iter() { + row.push(cell!(format!("{val}"))); + } + + let hs: HashSet<&u64> = hashes.iter().cloned().collect(); + if hs.len() > 1 { + row.push(cell!("X")); + } else { + row.push(cell!("")); + } + + clusters_table.add_row(Row::new(row)); + } + + clusters_table.printstd(); + Ok(()) +} + +fn print_responses_by_worker( + worker_responses: &WorkerResponses, + json: bool, +) -> Result<(), DisplayError> { + for (worker_id, content) in worker_responses.map.iter() { + println!("Worker {}", worker_id); + content.display(json)?; + } + + Ok(()) +} + +pub fn print_certificates_with_validity( + certs: &CertificatesWithFingerprints, +) -> Result<(), DisplayError> { + if certs.certs.is_empty() { + return Ok(println!("No certificates match your request.")); + } + + let mut table = Table::new(); + table.set_format(*prettytable::format::consts::FORMAT_CLEAN); + table.add_row(row![ + "fingeprint", + "valid not before", + "valide not after", + "domain names", + ]); + + for (fingerprint, cert) in &certs.certs { + let (_unparsed, pem_certificate) = + x509_parser::pem::parse_x509_pem(cert.certificate.as_bytes()) + .expect("Could not parse pem certificate"); + + let x509_certificate = pem_certificate + .parse_x509() + .expect("Could not parse x509 certificate"); + + let validity = x509_certificate.validity(); + + table.add_row(row!( + fingerprint, + format_datetime(validity.not_before)?, + format_datetime(validity.not_after)?, + concatenate_vector(&cert.names), + )); + } + table.printstd(); + + Ok(()) +} + +fn print_certificates_by_address(list: &ListOfCertificatesByAddress) -> Result<(), DisplayError> { + for certs in list.certificates.iter() { + println!("\t{}:", certs.address); + + for summary in certs.certificate_summaries.iter() { + println!("\t\t{}", summary); + } + } + Ok(()) +} + +fn print_request_counts(request_counts: &RequestCounts) -> Result<(), DisplayError> { + let mut table = Table::new(); + table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); + table.add_row(row!["request type", "count"]); + + for (request_type, count) in &request_counts.map { + table.add_row(row!(request_type, count)); + } + table.printstd(); + Ok(()) +} + +fn format_tags_to_string(tags: &BTreeMap) -> String { + tags.iter() + .map(|(k, v)| format!("{k}={v}")) + .collect::>() + .join(", ") +} + +fn list_string_vec(vec: &[String]) -> String { + let mut output = String::new(); + for item in vec.iter() { + output.push_str(item); + output.push('\n'); + } + output +} + +// ISO 8601 +fn format_datetime(asn1_time: ASN1Time) -> Result { + let datetime = asn1_time.to_datetime(); + + let formatted = datetime + .format(&format_description::well_known::Iso8601::DEFAULT) + .map_err(|_| DisplayError::DateTime)?; + Ok(formatted) +} + +/// Creates an empty table of the form +/// ```text +/// ┌────────────┬─────────────┬───────────┬────────┐ +/// │ │ header │ header │ header │ +/// ├────────────┼─────────────┼───────────┼────────┤ +/// │ cluster_id │ │ │ │ +/// ├────────────┼─────────────┼───────────┼────────┤ +/// │ cluster_id │ │ │ │ +/// ├────────────┼─────────────┼───────────┼────────┤ +/// │ cluster_id │ │ │ │ +/// └────────────┴─────────────┴───────────┴────────┘ +/// ``` +fn create_cluster_table(headers: Vec<&str>, data: &BTreeMap) -> Table { + let mut table = Table::new(); + table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS); + let mut row_header: Vec<_> = headers.iter().map(|h| cell!(h)).collect(); + for ref key in data.keys() { + row_header.push(cell!(&key)); + } + table.add_row(Row::new(row_header)); + table +} diff --git a/command/src/proto/mod.rs b/command/src/proto/mod.rs index 48f7972f6..c8567afd5 100644 --- a/command/src/proto/mod.rs +++ b/command/src/proto/mod.rs @@ -1,9 +1,25 @@ +use prost::DecodeError; + /// Contains all types received by and sent from Sōzu pub mod command; /// Implementation of fmt::Display for the protobuf types, used in the CLI pub mod display; +#[derive(thiserror::Error, Debug)] +pub enum DisplayError { + #[error("Could not display content")] + DisplayContent(String), + #[error("Error while parsing response to JSON")] + Json(serde_json::Error), + #[error("got the wrong response content type: {0}")] + WrongResponseType(String), + #[error("Could not format the datetime to ISO 8601")] + DateTime, + #[error("unrecognized protobuf variant: {0}")] + DecodeError(DecodeError), +} + // Simple helper to build ResponseContent from ContentType impl From for command::ResponseContent { fn from(value: command::response_content::ContentType) -> Self { From 95de156c533a67dde6e7135bf0e5bf96b7ea4cb6 Mon Sep 17 00:00:00 2001 From: Emmanuel Bosquet Date: Wed, 22 Nov 2023 10:08:56 +0100 Subject: [PATCH 3/4] display no other lines than JSON so that the output of --json commands is pipeable --- bin/src/ctl/command.rs | 21 +++++++++++++++++---- command/src/proto/display.rs | 7 ++++++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/bin/src/ctl/command.rs b/bin/src/ctl/command.rs index adb069133..43822c4b4 100644 --- a/bin/src/ctl/command.rs +++ b/bin/src/ctl/command.rs @@ -30,10 +30,15 @@ impl CommandManager { match response.status() { ResponseStatus::Processing => { - debug!("Proxy is processing: {}", response.message); + if !self.json { + debug!("Proxy is processing: {}", response.message); + } } ResponseStatus::Failure => bail!("Request failed: {}", response.message), ResponseStatus::Ok => { + if !self.json { + info!("{}", response.message); + } response.display(self.json)?; break; } @@ -55,7 +60,9 @@ impl CommandManager { match response.status() { ResponseStatus::Processing => { - debug!("Processing: {}", response.message); + if !self.json { + debug!("Processing: {}", response.message); + } } ResponseStatus::Failure => { bail!( @@ -143,7 +150,11 @@ impl CommandManager { let response = self.read_channel_message_with_timeout()?; match response.status() { - ResponseStatus::Processing => info!("Proxy is processing: {}", response.message), + ResponseStatus::Processing => { + if !self.json { + info!("Proxy is processing: {}", response.message); + } + } ResponseStatus::Failure => bail!( "could not stop the worker {}: {}", worker_id, @@ -186,7 +197,9 @@ impl CommandManager { match response.status() { ResponseStatus::Processing => { - debug!("Proxy is processing: {}", response.message); + if !self.json { + debug!("Proxy is processing: {}", response.message); + } } ResponseStatus::Failure | ResponseStatus::Ok => { response.display(self.json)?; diff --git a/command/src/proto/display.rs b/command/src/proto/display.rs index 3b7f02b5c..0a3796d47 100644 --- a/command/src/proto/display.rs +++ b/command/src/proto/display.rs @@ -114,7 +114,12 @@ pub fn print_json_response(input: &T) -> Result<(), Displ impl Response { pub fn display(&self, json: bool) -> Result<(), DisplayError> { match self.status() { - ResponseStatus::Ok => println!("Success: {}", self.message), + ResponseStatus::Ok => { + // avoid displaying anything else than JSON + if !json { + println!("Success: {}", self.message) + } + } ResponseStatus::Failure => println!("Failure: {}", self.message), ResponseStatus::Processing => { return Err(DisplayError::WrongResponseType( From 86303a2183a555c5c5c55d08acd3310ceeff0a94 Mon Sep 17 00:00:00 2001 From: Emmanuel Bosquet Date: Wed, 22 Nov 2023 11:17:05 +0100 Subject: [PATCH 4/4] ConfigState::cluster_state return Option display "no cluster found" if response is empty --- bin/src/command/requests.rs | 4 +-- command/src/proto/display.rs | 5 +++ command/src/state.rs | 64 ++++++++++++++++++++---------------- lib/src/server.rs | 8 +++-- 4 files changed, 49 insertions(+), 32 deletions(-) diff --git a/bin/src/command/requests.rs b/bin/src/command/requests.rs index e3072e68b..5eae67c7b 100644 --- a/bin/src/command/requests.rs +++ b/bin/src/command/requests.rs @@ -1020,7 +1020,7 @@ impl CommandServer { ), Some(RequestType::QueryClusterById(cluster_id)) => Some( ContentType::Clusters(ClusterInformations { - vec: vec![self.state.cluster_state(cluster_id)], + vec: self.state.cluster_state(cluster_id).into_iter().collect(), }) .into(), ), @@ -1030,7 +1030,7 @@ impl CommandServer { .get_cluster_ids_by_domain(domain.hostname.clone(), domain.path.clone()); let vec = cluster_ids .iter() - .map(|cluster_id| self.state.cluster_state(cluster_id)) + .filter_map(|cluster_id| self.state.cluster_state(cluster_id)) .collect(); Some(ContentType::Clusters(ClusterInformations { vec }).into()) } diff --git a/command/src/proto/display.rs b/command/src/proto/display.rs index 0a3796d47..6220f3ff1 100644 --- a/command/src/proto/display.rs +++ b/command/src/proto/display.rs @@ -634,6 +634,11 @@ fn print_cluster_infos(worker_responses: &WorkerResponses) -> Result<(), Display } } + if cluster_infos.is_empty() { + println!("no cluster found"); + return Ok(()); + } + println!("Cluster level configuration:\n"); for (cluster_info, workers_the_cluster_is_present_on) in cluster_infos.iter() { diff --git a/command/src/state.rs b/command/src/state.rs index 04af19958..2a6aca3ed 100644 --- a/command/src/state.rs +++ b/command/src/state.rs @@ -1173,43 +1173,51 @@ impl ConfigState { /// Gives details about a given cluster. /// Types like `HttpFrontend` are converted into protobuf ones, like `RequestHttpFrontend` - pub fn cluster_state(&self, cluster_id: &str) -> ClusterInformation { - let mut http_frontends = Vec::new(); - let mut https_frontends = Vec::new(); - let mut tcp_frontends = Vec::new(); - let mut backends = Vec::new(); - - for http_frontend in self.http_fronts.values() { - if let Some(id) = &http_frontend.cluster_id { - if id == cluster_id { - http_frontends.push(http_frontend.clone().into()); - } - } + pub fn cluster_state(&self, cluster_id: &str) -> Option { + let configuration = self.clusters.get(cluster_id).cloned(); + if configuration.is_none() { + return None; } - for https_frontend in self.https_fronts.values() { - if let Some(id) = &https_frontend.cluster_id { - if id == cluster_id { - https_frontends.push(https_frontend.clone().into()); - } - } - } + let http_frontends: Vec = self + .http_fronts + .values() + .filter(|front| front.cluster_id.as_deref() == Some(cluster_id)) + .map(|front| front.clone().into()) + .collect(); - for tcp_f in self.tcp_fronts.get(cluster_id).cloned().unwrap_or_default() { - tcp_frontends.push(tcp_f.clone().into()); - } + let https_frontends: Vec = self + .https_fronts + .values() + .filter(|front| front.cluster_id.as_deref() == Some(cluster_id)) + .map(|front| front.clone().into()) + .collect(); - for backend in self.backends.get(cluster_id).cloned().unwrap_or_default() { - backends.push(backend.clone().into()) - } + let tcp_frontends: Vec = self + .tcp_fronts + .get(cluster_id) + .cloned() + .unwrap_or_default() + .iter() + .map(|front| front.clone().into()) + .collect(); - ClusterInformation { - configuration: self.clusters.get(cluster_id).cloned(), + let backends: Vec = self + .backends + .get(cluster_id) + .cloned() + .unwrap_or_default() + .iter() + .map(|backend| backend.clone().into()) + .collect(); + + Some(ClusterInformation { + configuration, http_frontends, https_frontends, tcp_frontends, backends, - } + }) } pub fn count_backends(&self) -> usize { diff --git a/lib/src/server.rs b/lib/src/server.rs index 0461dbe41..c6f534735 100644 --- a/lib/src/server.rs +++ b/lib/src/server.rs @@ -898,7 +898,11 @@ impl Server { push_queue(WorkerResponse::ok_with_content( message.id.clone(), ContentType::Clusters(ClusterInformations { - vec: vec![self.config_state.cluster_state(cluster_id)], + vec: self + .config_state + .cluster_state(cluster_id) + .into_iter() + .collect(), }) .into(), )); @@ -909,7 +913,7 @@ impl Server { .get_cluster_ids_by_domain(domain.hostname.clone(), domain.path.clone()); let vec = cluster_ids .iter() - .map(|cluster_id| self.config_state.cluster_state(cluster_id)) + .filter_map(|cluster_id| self.config_state.cluster_state(cluster_id)) .collect(); push_queue(WorkerResponse::ok_with_content(