From f97d7cb4c7b78f7a3fdffc8b35ed996242909bb5 Mon Sep 17 00:00:00 2001 From: Chris Holcombe Date: Tue, 15 Oct 2019 13:50:18 -0700 Subject: [PATCH 1/2] Split sensu module into library --- Cargo.toml | 5 + sensu/Cargo.toml | 14 + sensu/src/client.rs | 506 +++++++++++++++++++++++++++ {src/sensu => sensu/src}/endpoint.rs | 0 sensu/src/err.rs | 50 +++ {src/sensu => sensu/src}/expire.rs | 0 sensu/src/lib.rs | 10 + sensu/src/opts.rs | 26 ++ {src/sensu => sensu/src}/payload.rs | 2 +- {src/sensu => sensu/src}/resource.rs | 0 {src => sensu/src}/resources.rs | 0 src/main.rs | 7 +- src/opts.rs | 31 +- src/sensu/client.rs | 381 -------------------- src/sensu/mod.rs | 16 - 15 files changed, 619 insertions(+), 429 deletions(-) create mode 100644 sensu/Cargo.toml create mode 100644 sensu/src/client.rs rename {src/sensu => sensu/src}/endpoint.rs (100%) create mode 100644 sensu/src/err.rs rename {src/sensu => sensu/src}/expire.rs (100%) create mode 100644 sensu/src/lib.rs create mode 100644 sensu/src/opts.rs rename {src/sensu => sensu/src}/payload.rs (98%) rename {src/sensu => sensu/src}/resource.rs (100%) rename {src => sensu/src}/resources.rs (100%) delete mode 100644 src/sensu/client.rs delete mode 100644 src/sensu/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 1f402b4..79f8e78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "shush" version = "0.2.3" +edition = "2018" authors = ["John Baublitz "] description = "A management tool for silencing Sensu checks written in Rust" repository = "https://github.com/threatstack/shush" @@ -12,12 +13,16 @@ include = [ "LICENSE", ] +[workspace] +members = ["sensu"] + [dependencies] clap = "2.33.0" hyper = "0.12" itertools = "0.6.0" nom = "5.0" serde_json = "1.0.24" +sensu = {path = "sensu" } regex = "1.1.0" rust-ini = "0.10.0" tokio = "0.1" diff --git a/sensu/Cargo.toml b/sensu/Cargo.toml new file mode 100644 index 0000000..11ebe78 --- /dev/null +++ b/sensu/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "sensu" +version = "0.1.0" +authors = ["Chris Holcombe "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +hyper = "0.12" +itertools = "0.6.0" +regex = "1.1.0" +serde_json = "1.0.24" +tokio = "0.1" \ No newline at end of file diff --git a/sensu/src/client.rs b/sensu/src/client.rs new file mode 100644 index 0000000..b7ffad5 --- /dev/null +++ b/sensu/src/client.rs @@ -0,0 +1,506 @@ +use std::{ + borrow::Borrow, + collections::{HashMap, HashSet}, + convert::TryInto, + error::Error, + fmt::Display, + process, +}; + +use hyper::{ + client::HttpConnector, + header::{self, HeaderValue}, + rt::{Future, Stream}, + Body, Client, Method, Request, StatusCode, Uri, +}; +use itertools::iproduct; +use regex::{Regex, RegexBuilder}; +use serde_json::{self, Map, Value}; +use tokio::runtime::Runtime; + +use super::*; +use crate::{endpoint::SensuEndpoint, payload::SensuPayload, resource::SensuResource}; +use err::SensuError; +use opts::{ClearOpts, ListOpts, SilenceOpts}; +use resources::{ShushResourceType, ShushResources}; + +pub struct SensuClient(Client, Runtime, Uri); + +impl SensuClient { + pub fn new(base_url: String) -> Result> { + Ok(SensuClient( + Client::builder().build(HttpConnector::new(4)), + Runtime::new()?, + base_url.parse::()?, + )) + } + + pub fn request( + &mut self, + method: Method, + uri: U, + body: Option, + ) -> Result, SensuError> + where + U: TryInto, + U::Error: Display, + { + let mut full_uri = uri.try_into().map_err(|e| SensuError::new_string(e))?; + let map: Option> = body.map(|b| b.into()); + if full_uri.authority_part().is_none() { + let mut parts = full_uri.into_parts(); + let base_uri = self.2.clone().into_parts(); + parts.scheme = base_uri.scheme; + parts.authority = base_uri.authority; + full_uri = Uri::from_parts(parts).map_err(|e| SensuError::new(e.description()))?; + } + + let mut builder = Request::builder(); + builder.method(method).uri(full_uri); + let req = if let Some(ref m) = map { + let body_string = + serde_json::to_string(m).map_err(|e| SensuError::new(e.description()))?; + builder + .header(header::CONTENT_LENGTH, body_string.len()) + .header( + header::CONTENT_TYPE, + HeaderValue::from_static("application/json"), + ) + .body(Body::from(body_string)) + .map_err(|e| SensuError::new(e.description()))? + } else { + builder + .body(Body::empty()) + .map_err(|e| SensuError::new(e.description()))? + }; + + self.1.block_on( + self.0 + .request(req) + .map_err(|e| SensuError::from(e)) + .and_then(|resp| { + if resp.status() == StatusCode::NOT_FOUND { + return Err(SensuError::not_found()); + } + Ok(resp) + }) + .and_then(|resp| resp.into_body().concat2().map_err(|e| SensuError::from(e))) + .and_then(|chunk| { + if chunk.len() < 1 { + return Ok(None); + } + serde_json::from_slice::(&chunk) + .map_err(|e| { + println!( + "Response: {}", + match std::str::from_utf8(&chunk) + .map_err(|e| { SensuError::new(&e.to_string()) }) + { + Ok(j) => j, + Err(e) => return e, + } + ); + SensuError::new(&e.to_string()) + }) + .map(Some) + }), + ) + } + + fn get_node_to_client_map(&mut self) -> Result, Box> { + let clients = self.request(Method::GET, SensuEndpoint::Clients, None)?; + + let mut client_map = HashMap::new(); + if let Some(Value::Array(v)) = clients { + for mut item in v { + let iid = item + .as_object_mut() + .and_then(|obj| obj.remove("instance_id")); + let iid_string = match iid { + Some(Value::String(string)) => Some(string), + _ => None, + }; + + let client = item + .as_object_mut() + .and_then(|client| client.remove("name")); + let client_string = match client { + Some(Value::String(string)) => Some(string), + _ => None, + }; + + if let (Some(i), Some(c)) = (iid_string, client_string) { + client_map.insert(i, c); + } + } + } + + Ok(client_map) + } + + fn map_to_sensu_resources( + &mut self, + res: ShushResources, + ) -> Result, Box> { + let (resource_type, resources) = (res.res_type, res.resources); + let mut map = self.get_node_to_client_map()?; + let mapped_resources = match resource_type { + ShushResourceType::Node => resources + .iter() + .filter_map(|v| { + if let Some(val) = map.remove(v) { + if self.validate_client(val.as_str()) { + Some(SensuResource::Client(val)) + } else { + None + } + } else { + println!( + r#"WARNING: instance ID "{}" not associated with Sensu client ID"#, + v + ); + println!( + "If you recently provisioned an instance, please wait for it to \ + register with Sensu" + ); + println!(); + None + } + }) + .collect(), + ShushResourceType::Sub => { + let subs = resources + .into_iter() + .map(SensuResource::Subscription) + .collect(); + self.validate_subscriptions(subs) + } + ShushResourceType::Client => resources + .into_iter() + .filter_map(|c| { + if self.validate_client(c.as_str()) { + Some(SensuResource::Client(c)) + } else { + None + } + }) + .collect(), + }; + Ok(mapped_resources) + } + + fn validate_client(&mut self, client_name: &str) -> bool { + let resp = self.request(Method::GET, SensuEndpoint::Client(client_name), None); + if let Err(SensuError::NotFound) = resp { + return false; + } else { + return true; + } + } + + fn validate_subscriptions(&mut self, subscriptions: Vec) -> Vec { + let print_error = || { + println!("Failed to pull data from API for subscriptions"); + println!("Proceeding without subscription validation"); + }; + + let resp_res = self.request(Method::GET, SensuEndpoint::Clients, None); + let resp = match resp_res { + Err(SensuError::NotFound) => { + print_error(); + return subscriptions; + } + Err(SensuError::Message(s)) => { + println!("{}", s); + return subscriptions; + } + Ok(resp) => resp, + }; + + let subs: HashSet = if let Some(Value::Array(vec)) = resp { + vec.into_iter() + .filter_map(|obj| { + obj.as_object() + .and_then(|o| o.get("subscriptions")) + .and_then(|subs| subs.as_array()) + .map(|arr| { + let v: Vec = arr + .into_iter() + .filter_map(|s| s.as_str().map(|st| st.to_string())) + .collect(); + v + }) + }) + .flatten() + .collect() + } else { + print_error(); + return subscriptions; + }; + + subscriptions + .into_iter() + .filter(|sub| { + let string: &String = sub.borrow(); + if subs.contains(string) { + true + } else { + println!("Subscription {} does not exist - filtering...", sub); + false + } + }) + .collect() + } + + fn validate_checks(&mut self, checks: Vec) -> Vec { + let print_error = || { + println!("Failed to pull data from API for check results"); + println!("Proceeding without check validation"); + }; + + let results_res = self.request(Method::GET, SensuEndpoint::Results, None); + let results = match results_res { + Err(SensuError::NotFound) => { + print_error(); + return checks; + } + Err(SensuError::Message(s)) => { + println!("{}", s); + return checks; + } + Ok(res) => res, + }; + + let hs: HashSet = if let Some(Value::Array(vec)) = results { + vec.into_iter() + .filter_map(|obj| { + obj.as_object() + .and_then(|o| o.get("check")) + .and_then(|c| c.get("name")) + .and_then(|n| n.as_str().map(|st| st.to_string())) + }) + .collect() + } else { + print_error(); + return checks; + }; + + let filtered_checks: Vec = checks + .into_iter() + .filter(|chk| { + if hs.contains(chk) { + true + } else { + println!("Check {} does not exist - filtering...", chk); + false + } + }) + .collect(); + + filtered_checks + } + + pub fn silence(&mut self, s: SilenceOpts) -> Result<(), Box> { + let resources: Option> = match s.resources { + Some(res) => Some( + self.map_to_sensu_resources(res)? + .into_iter() + .map(|r| format!("{}", r)) + .collect(), + ), + None => None, + }; + let checks = s.checks.map(|cks| self.validate_checks(cks)); + let expire = s.expire; + match (resources, checks) { + (Some(res), Some(chk)) => iproduct!(res, chk).for_each(|(r, c)| { + println!( + "Silencing check {} on resource {} and will {}", + c, r, expire + ); + let _ = self + .request( + Method::POST, + SensuEndpoint::Silenced, + Some(SensuPayload { + res: Some(r), + chk: Some(c), + expire: Some(expire.clone()), + }), + ) + .map_err(|e| { + println!("{}", e); + process::exit(1); + }); + }), + (Some(res), None) => res.into_iter().for_each(|r| { + println!("Silencing all checks on resource {} and will {}", r, expire); + let _ = self + .request( + Method::POST, + SensuEndpoint::Silenced, + Some(SensuPayload { + res: Some(r), + chk: None, + expire: Some(expire.clone()), + }), + ) + .map_err(|e| { + println!("{}", e); + process::exit(1); + }); + }), + (None, Some(chk)) => chk.into_iter().for_each(|c| { + println!( + "Silencing checks {} on all resources and will {}", + c, expire + ); + let _ = self + .request( + Method::POST, + SensuEndpoint::Silenced, + Some(SensuPayload { + res: None, + chk: Some(c), + expire: Some(expire.clone()), + }), + ) + .map_err(|e| { + println!("{}", e); + process::exit(1); + }); + }), + (_, _) => { + println!("No targets specified - Exiting..."); + process::exit(1); + } + }; + Ok(()) + } + + pub fn clear(&mut self, s: ClearOpts) -> Result<(), Box> { + let resources: Option> = match s.resources { + Some(res) => Some( + self.map_to_sensu_resources(res)? + .into_iter() + .map(|r| format!("{}", r)) + .collect(), + ), + None => None, + }; + let checks = s.checks; + match (resources, checks) { + (Some(res), Some(chk)) => iproduct!(res, chk).for_each(|(r, c)| { + println!("Clearing silences on checks {} on resources {}", c, r); + let _ = self + .request( + Method::POST, + SensuEndpoint::Clear, + Some(SensuPayload { + res: Some(r), + chk: Some(c), + expire: None, + }), + ) + .map_err(|e| { + println!("{}", e); + process::exit(1); + }); + }), + (Some(res), None) => res.into_iter().for_each(|r| { + println!("Clearing silences on all checks on resources {}", r); + let _ = self + .request( + Method::POST, + SensuEndpoint::Clear, + Some(SensuPayload { + res: Some(r), + chk: None, + expire: None, + }), + ) + .map_err(|e| { + println!("{}", e); + process::exit(1); + }); + }), + (None, Some(chk)) => chk.into_iter().for_each(|c| { + println!("Clearing silences on checks {} on all resources", c); + let _ = self + .request( + Method::POST, + SensuEndpoint::Clear, + Some(SensuPayload { + res: None, + chk: Some(c), + expire: None, + }), + ) + .map_err(|e| { + println!("{}", e); + process::exit(1); + }); + }), + (_, _) => { + println!("No targets specified - Exiting..."); + process::exit(1); + } + }; + Ok(()) + } + + pub fn list(&mut self, s: ListOpts) -> Result<(), Box> { + let compile_regex = |string: Option<&str>| -> Result> { + let regex = RegexBuilder::new(string.unwrap_or(".*")) + .size_limit(8192) + .dfa_size_limit(8192) + .build()?; + Ok(regex) + }; + + let sub_regex = compile_regex(s.sub.as_ref().map(|s| s.as_str()))?; + let chk_regex = compile_regex(s.chk.as_ref().map(|s| s.as_str()))?; + + println!("Active silences:"); + let resp = self.request(Method::GET, SensuEndpoint::Silenced, None)?; + if let Some(Value::Array(v)) = resp { + if v.len() == 0 { + println!("\tNo silences"); + return Ok(()); + } + for obj in v { + if let Value::Object(o) = obj { + let user = o + .get("creator") + .and_then(|c| c.as_str()) + .unwrap_or("unknown"); + let subscription = o + .get("subscription") + .and_then(|c| c.as_str()) + .unwrap_or("all"); + let check = o.get("check").and_then(|c| c.as_str()).unwrap_or("all"); + let expire = o.get("expire").and_then(|c| c.as_u64()); + let eor = o + .get("expire_on_resolve") + .and_then(|c| c.as_bool()) + .unwrap_or(false); + + if !sub_regex.is_match(subscription) || !chk_regex.is_match(check) { + continue; + } + + println!("\tSubscription:\t\t{}", subscription); + println!("\tCheck:\t\t\t{}", check); + match expire { + Some(num) => println!("\tExpiration:\t\t{}", num), + None => println!("\tExpiration:\t\tnever"), + }; + println!("\tExpire on resolve:\t{}", eor); + println!("\tUser:\t\t\t{}", user); + println!(); + } + } + } + Ok(()) + } +} diff --git a/src/sensu/endpoint.rs b/sensu/src/endpoint.rs similarity index 100% rename from src/sensu/endpoint.rs rename to sensu/src/endpoint.rs diff --git a/sensu/src/err.rs b/sensu/src/err.rs new file mode 100644 index 0000000..d7ea3d1 --- /dev/null +++ b/sensu/src/err.rs @@ -0,0 +1,50 @@ +//! Error type and conversions for internal shush error handling + +use std::fmt; +use std::fmt::{Formatter,Display}; +use std::error::Error; + +use hyper; + +/// Error type for passing error messages to display from the CLI +#[derive(Debug)] +pub enum SensuError { + NotFound, + Message(String), +} + +impl SensuError { + pub fn new(msg: &str) -> Self { + SensuError::Message(msg.to_string()) + } + + pub fn new_string(any_format: F) -> Self where F: Display { + SensuError::Message(format!("{}", any_format)) + } + + pub fn not_found() -> Self { + SensuError::NotFound + } +} + +impl From for SensuError { + fn from(e: hyper::Error) -> Self { + SensuError::new(e.description()) + } +} + +impl Error for SensuError { + fn description(&self) -> &str { + if let SensuError::Message(string) = self { + string.as_ref() + } else { + "404 Not found" + } + } +} + +impl Display for SensuError { + fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { + write!(f, "{}", self.description()) + } +} diff --git a/src/sensu/expire.rs b/sensu/src/expire.rs similarity index 100% rename from src/sensu/expire.rs rename to sensu/src/expire.rs diff --git a/sensu/src/lib.rs b/sensu/src/lib.rs new file mode 100644 index 0000000..1a7661a --- /dev/null +++ b/sensu/src/lib.rs @@ -0,0 +1,10 @@ +//! Sensu API related request and response-parsing logic + +pub mod client; +pub mod endpoint; +pub mod err; +pub mod expire; +pub mod opts; +pub mod payload; +pub mod resource; +pub mod resources; \ No newline at end of file diff --git a/sensu/src/opts.rs b/sensu/src/opts.rs new file mode 100644 index 0000000..1627307 --- /dev/null +++ b/sensu/src/opts.rs @@ -0,0 +1,26 @@ +//! Generates Shush data structures for `sensu` module from command line flags + +use crate::resources::ShushResources; +use crate::expire::Expire; + +pub struct SilenceOpts { + pub resources: Option, + pub checks: Option>, + pub expire: Expire, +} + +pub struct ClearOpts { + pub resources: Option, + pub checks: Option>, +} + +pub struct ListOpts { + pub sub: Option, + pub chk: Option, +} + +pub enum ShushOpts { + Silence(SilenceOpts), + Clear(ClearOpts), + List(ListOpts), +} diff --git a/src/sensu/payload.rs b/sensu/src/payload.rs similarity index 98% rename from src/sensu/payload.rs rename to sensu/src/payload.rs index 0ee67c2..a1c1030 100644 --- a/src/sensu/payload.rs +++ b/sensu/src/payload.rs @@ -2,7 +2,7 @@ use std::env; use serde_json::{Value,Map,Number}; -use super::Expire; +use crate::expire::Expire; /// Generic struct for any Sensu payload - can be used for clear or silence #[derive(Debug)] diff --git a/src/sensu/resource.rs b/sensu/src/resource.rs similarity index 100% rename from src/sensu/resource.rs rename to sensu/src/resource.rs diff --git a/src/resources.rs b/sensu/src/resources.rs similarity index 100% rename from src/resources.rs rename to sensu/src/resources.rs diff --git a/src/main.rs b/src/main.rs index 65bffb5..c478831 100644 --- a/src/main.rs +++ b/src/main.rs @@ -135,21 +135,16 @@ extern crate tokio; extern crate serde_json; -#[macro_use] -extern crate itertools; extern crate ini; extern crate nom; mod config; mod err; mod opts; -mod resources; -mod sensu; use std::error::Error; -use opts::ShushOpts; -use sensu::SensuClient; +use sensu::{client::SensuClient, opts::ShushOpts}; /// Main function - handle arg parsing and all executable actions pub fn main() -> Result<(), Box> { diff --git a/src/opts.rs b/src/opts.rs index 914d6af..0d67efc 100644 --- a/src/opts.rs +++ b/src/opts.rs @@ -5,31 +5,12 @@ use std::process; use clap::{App,Arg,ArgMatches}; use regex::Regex; -use config::ShushConfig; -use resources::{ShushResources,ShushResourceType}; -use sensu::Expire; - -pub struct SilenceOpts { - pub resources: Option, - pub checks: Option>, - pub expire: Expire, -} - -pub struct ClearOpts { - pub resources: Option, - pub checks: Option>, -} - -pub struct ListOpts { - pub sub: Option, - pub chk: Option, -} - -pub enum ShushOpts { - Silence(SilenceOpts), - Clear(ClearOpts), - List(ListOpts), -} +use crate::config::ShushConfig; +use sensu::resources::{ShushResources,ShushResourceType}; +use sensu::{ + opts::{ClearOpts, ShushOpts, SilenceOpts, ListOpts}, + expire::Expire +}; pub fn get_expiration(expire: String, expire_on_resolve: bool) -> Expire { if expire.as_str() == "none" { diff --git a/src/sensu/client.rs b/src/sensu/client.rs deleted file mode 100644 index b707255..0000000 --- a/src/sensu/client.rs +++ /dev/null @@ -1,381 +0,0 @@ -use std::borrow::Borrow; -use std::collections::{HashMap,HashSet}; -use std::convert::TryInto; -use std::error::Error; -use std::fmt::Display; -use std::process; - -use serde_json::{self,Value,Map}; -use hyper::{Body,Client,Method,Request,StatusCode,Uri}; -use hyper::client::HttpConnector; -use hyper::header::{self,HeaderValue}; -use hyper::rt::{Future,Stream}; -use regex::{Regex,RegexBuilder}; -use tokio::runtime::Runtime; - -use super::*; -use err::SensuError; -use opts::{ClearOpts,ListOpts,SilenceOpts}; -use resources::{ShushResources,ShushResourceType}; - -pub struct SensuClient(Client, Runtime, Uri); - -impl SensuClient { - pub fn new(base_url: String) -> Result> { - Ok(SensuClient(Client::builder().build(HttpConnector::new(4)), Runtime::new()?, - base_url.parse::()?)) - } - - pub fn request(&mut self, method: Method, uri: U, body: Option) - -> Result, SensuError> where U: TryInto, U::Error: Display { - let mut full_uri = uri.try_into().map_err(|e| SensuError::new_string(e))?; - let map: Option> = body.map(|b| b.into()); - if full_uri.authority_part().is_none() { - let mut parts = full_uri.into_parts(); - let base_uri = self.2.clone().into_parts(); - parts.scheme = base_uri.scheme; - parts.authority = base_uri.authority; - full_uri = Uri::from_parts(parts).map_err(|e| SensuError::new(e.description()))?; - } - - let mut builder = Request::builder(); - builder.method(method).uri(full_uri); - let req = if let Some(ref m) = map { - let body_string = serde_json::to_string(m).map_err(|e| { - SensuError::new(e.description()) - })?; - builder.header(header::CONTENT_LENGTH, body_string.len()) - .header(header::CONTENT_TYPE, HeaderValue::from_static("application/json")) - .body(Body::from(body_string)) - .map_err(|e| SensuError::new(e.description()))? - } else { - builder.body(Body::empty()).map_err(|e| SensuError::new(e.description()))? - }; - - self.1.block_on(self.0.request(req).map_err(|e| { - SensuError::from(e) - }).and_then(|resp| { - if resp.status() == StatusCode::NOT_FOUND { - return Err(SensuError::not_found()); - } - Ok(resp) - }).and_then(|resp| { - resp.into_body().concat2().map_err(|e| SensuError::from(e)) - }).and_then(|chunk| { - if chunk.len() < 1 { - return Ok(None); - } - serde_json::from_slice::(&chunk).map_err(|e| { - println!("Response: {}", match std::str::from_utf8(&chunk).map_err(|e| { - SensuError::new(&e.to_string()) - }) { - Ok(j) => j, - Err(e) => return e, - }); - SensuError::new(&e.to_string()) - }).map(Some) - })) - } - - fn get_node_to_client_map(&mut self) -> Result, Box> { - let clients = self.request(Method::GET, SensuEndpoint::Clients, None)?; - - let mut client_map = HashMap::new(); - if let Some(Value::Array(v)) = clients { - for mut item in v { - let iid = item.as_object_mut().and_then(|obj| obj.remove("instance_id")); - let iid_string = match iid { - Some(Value::String(string)) => Some(string), - _ => None, - }; - - let client = item.as_object_mut().and_then(|client| client.remove("name")); - let client_string = match client { - Some(Value::String(string)) => Some(string), - _ => None, - }; - - if let (Some(i), Some(c)) = (iid_string, client_string) { - client_map.insert(i, c); - } - } - } - - Ok(client_map) - } - - fn map_to_sensu_resources(&mut self, res: ShushResources) - -> Result, Box>{ - let (resource_type, resources) = (res.res_type, res.resources); - let mut map = self.get_node_to_client_map()?; - let mapped_resources = match resource_type { - ShushResourceType::Node => resources.iter().filter_map(|v| { - if let Some(val) = map.remove(v) { - if self.validate_client(val.as_str()) { - Some(SensuResource::Client(val)) - } else { - None - } - } else { - println!(r#"WARNING: instance ID "{}" not associated with Sensu client ID"#, v); - println!("If you recently provisioned an instance, please wait for it to \ - register with Sensu"); - println!(); - None - } - }).collect(), - ShushResourceType::Sub => { - let subs = resources.into_iter().map(SensuResource::Subscription).collect(); - self.validate_subscriptions(subs) - }, - ShushResourceType::Client => resources.into_iter() - .filter_map(|c| { - if self.validate_client(c.as_str()) { - Some(SensuResource::Client(c)) - } else { - None - } - }).collect(), - }; - Ok(mapped_resources) - } - - fn validate_client(&mut self, client_name: &str) -> bool { - let resp = self.request(Method::GET, SensuEndpoint::Client(client_name), None); - if let Err(SensuError::NotFound) = resp { - return false; - } else { - return true; - } - } - - fn validate_subscriptions(&mut self, subscriptions: Vec) -> Vec { - let print_error = || { - println!("Failed to pull data from API for subscriptions"); - println!("Proceeding without subscription validation"); - }; - - let resp_res = self.request(Method::GET, SensuEndpoint::Clients, None); - let resp = match resp_res { - Err(SensuError::NotFound) => { - print_error(); - return subscriptions; - }, - Err(SensuError::Message(s)) => { - println!("{}", s); - return subscriptions; - }, - Ok(resp) => resp, - }; - - let subs: HashSet = if let Some(Value::Array(vec)) = resp { - vec.into_iter().filter_map(|obj| { - obj.as_object().and_then(|o| o.get("subscriptions")) - .and_then(|subs| subs.as_array()).map(|arr| { - let v: Vec = arr.into_iter() - .filter_map(|s| s.as_str().map(|st| st.to_string())).collect(); - v - }) - }).flatten().collect() - } else { - print_error(); - return subscriptions; - }; - - subscriptions.into_iter().filter(|sub| { - let string: &String = sub.borrow(); - if subs.contains(string) { - true - } else { - println!("Subscription {} does not exist - filtering...", sub); - false - } - }).collect() - } - - fn validate_checks(&mut self, checks: Vec) -> Vec { - let print_error = || { - println!("Failed to pull data from API for check results"); - println!("Proceeding without check validation"); - }; - - let results_res = self.request(Method::GET, SensuEndpoint::Results, None); - let results = match results_res { - Err(SensuError::NotFound) => { - print_error(); - return checks; - }, - Err(SensuError::Message(s)) => { - println!("{}", s); - return checks; - }, - Ok(res) => res, - }; - - let hs: HashSet = if let Some(Value::Array(vec)) = results { - vec.into_iter().filter_map(|obj| { - obj.as_object().and_then(|o| o.get("check")) - .and_then(|c| c.get("name")) - .and_then(|n| n.as_str().map(|st| st.to_string())) - }).collect() - } else { - print_error(); - return checks; - }; - - let filtered_checks: Vec = checks.into_iter().filter(|chk| { - if hs.contains(chk) { - true - } else { - println!("Check {} does not exist - filtering...", chk); - false - } - }).collect(); - - filtered_checks - } - - pub fn silence(&mut self, s: SilenceOpts) -> Result<(), Box> { - let resources: Option> = match s.resources { - Some(res) => Some(self.map_to_sensu_resources(res)?.into_iter() - .map(|r| format!("{}", r)).collect()), - None => None, - }; - let checks = s.checks.map(|cks| self.validate_checks(cks)); - let expire = s.expire; - match (resources, checks) { - (Some(res), Some(chk)) => iproduct!(res, chk).for_each(|(r, c)| { - println!("Silencing check {} on resource {} and will {}", c, r, expire); - let _ = self.request(Method::POST, SensuEndpoint::Silenced, Some(SensuPayload { - res: Some(r), - chk: Some(c), - expire: Some(expire.clone()), - })).map_err(|e| { - println!("{}", e); - process::exit(1); - }); - }), - (Some(res), None) => res.into_iter().for_each(|r| { - println!("Silencing all checks on resource {} and will {}", r, expire); - let _ = self.request(Method::POST, SensuEndpoint::Silenced, Some(SensuPayload { - res: Some(r), - chk: None, - expire: Some(expire.clone()), - })).map_err(|e| { - println!("{}", e); - process::exit(1); - }); - }), - (None, Some(chk)) => chk.into_iter().for_each(|c| { - println!("Silencing checks {} on all resources and will {}", c, expire); - let _ = self.request(Method::POST, SensuEndpoint::Silenced, Some(SensuPayload { - res: None, - chk: Some(c), - expire: Some(expire.clone()), - })).map_err(|e| { - println!("{}", e); - process::exit(1); - }); - }), - (_, _) => { - println!("No targets specified - Exiting..."); - process::exit(1); - }, - }; - Ok(()) - } - - pub fn clear(&mut self, s: ClearOpts) -> Result<(), Box> { - let resources: Option> = match s.resources { - Some(res) => Some(self.map_to_sensu_resources(res)?.into_iter() - .map(|r| format!("{}", r)).collect()), - None => None, - }; - let checks = s.checks; - match (resources, checks) { - (Some(res), Some(chk)) => iproduct!(res, chk).for_each(|(r, c)| { - println!("Clearing silences on checks {} on resources {}", c, r); - let _ = self.request(Method::POST, SensuEndpoint::Clear, Some(SensuPayload { - res: Some(r), - chk: Some(c), - expire: None, - })).map_err(|e| { - println!("{}", e); - process::exit(1); - }); - }), - (Some(res), None) => res.into_iter().for_each(|r| { - println!("Clearing silences on all checks on resources {}", r); - let _ = self.request(Method::POST, SensuEndpoint::Clear, Some(SensuPayload { - res: Some(r), - chk: None, - expire: None, - })).map_err(|e| { - println!("{}", e); - process::exit(1); - }); - }), - (None, Some(chk)) => chk.into_iter().for_each(|c| { - println!("Clearing silences on checks {} on all resources", c); - let _ = self.request(Method::POST, SensuEndpoint::Clear, Some(SensuPayload { - res: None, - chk: Some(c), - expire: None, - })).map_err(|e| { - println!("{}", e); - process::exit(1); - }); - }), - (_, _) => { - println!("No targets specified - Exiting..."); - process::exit(1); - }, - }; - Ok(()) - } - - pub fn list(&mut self, s: ListOpts) -> Result<(), Box> { - let compile_regex = |string: Option<&str>| -> Result> { - let regex = RegexBuilder::new(string.unwrap_or(".*")).size_limit(8192) - .dfa_size_limit(8192).build()?; - Ok(regex) - }; - - let sub_regex = compile_regex(s.sub.as_ref().map(|s| s.as_str()))?; - let chk_regex = compile_regex(s.chk.as_ref().map(|s| s.as_str()))?; - - println!("Active silences:"); - let resp = self.request(Method::GET, SensuEndpoint::Silenced, None)?; - if let Some(Value::Array(v)) = resp { - if v.len() == 0 { - println!("\tNo silences"); - return Ok(()); - } - for obj in v { - if let Value::Object(o) = obj { - let user = o.get("creator").and_then(|c| c.as_str()).unwrap_or("unknown"); - let subscription = o.get("subscription").and_then(|c| c.as_str()) - .unwrap_or("all"); - let check = o.get("check").and_then(|c| c.as_str()).unwrap_or("all"); - let expire = o.get("expire").and_then(|c| c.as_u64()); - let eor = o.get("expire_on_resolve").and_then(|c| c.as_bool()).unwrap_or(false); - - if !sub_regex.is_match(subscription) || !chk_regex.is_match(check) { - continue - } - - println!("\tSubscription:\t\t{}", subscription); - println!("\tCheck:\t\t\t{}", check); - match expire { - Some(num) => println!("\tExpiration:\t\t{}", num), - None => println!("\tExpiration:\t\tnever"), - }; - println!("\tExpire on resolve:\t{}", eor); - println!("\tUser:\t\t\t{}", user); - println!(); - } - } - } - Ok(()) - } -} diff --git a/src/sensu/mod.rs b/src/sensu/mod.rs deleted file mode 100644 index da770af..0000000 --- a/src/sensu/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Sensu API related request and response-parsing logic - -mod client; -pub use self::client::*; - -mod endpoint; -pub use self::endpoint::*; - -mod expire; -pub use self::expire::*; - -mod payload; -pub use self::payload::*; - -mod resource; -pub use self::resource::*; From c7e319e842370b541e140661cfb7e3af0568a4ad Mon Sep 17 00:00:00 2001 From: Chris Holcombe Date: Tue, 15 Oct 2019 13:56:15 -0700 Subject: [PATCH 2/2] Clippy fixes --- sensu/src/client.rs | 14 +++++++------- sensu/src/payload.rs | 2 +- sensu/src/resources.rs | 2 +- src/config.rs | 2 +- src/main.rs | 2 +- src/opts.rs | 17 ++++++++--------- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/sensu/src/client.rs b/sensu/src/client.rs index b7ffad5..4cfcbef 100644 --- a/sensu/src/client.rs +++ b/sensu/src/client.rs @@ -45,7 +45,7 @@ impl SensuClient { U: TryInto, U::Error: Display, { - let mut full_uri = uri.try_into().map_err(|e| SensuError::new_string(e))?; + let mut full_uri = uri.try_into().map_err(SensuError::new_string)?; let map: Option> = body.map(|b| b.into()); if full_uri.authority_part().is_none() { let mut parts = full_uri.into_parts(); @@ -77,14 +77,14 @@ impl SensuClient { self.1.block_on( self.0 .request(req) - .map_err(|e| SensuError::from(e)) + .map_err(SensuError::from) .and_then(|resp| { if resp.status() == StatusCode::NOT_FOUND { return Err(SensuError::not_found()); } Ok(resp) }) - .and_then(|resp| resp.into_body().concat2().map_err(|e| SensuError::from(e))) + .and_then(|resp| resp.into_body().concat2().map_err(SensuError::from)) .and_then(|chunk| { if chunk.len() < 1 { return Ok(None); @@ -192,9 +192,9 @@ impl SensuClient { fn validate_client(&mut self, client_name: &str) -> bool { let resp = self.request(Method::GET, SensuEndpoint::Client(client_name), None); if let Err(SensuError::NotFound) = resp { - return false; + false } else { - return true; + true } } @@ -225,7 +225,7 @@ impl SensuClient { .and_then(|subs| subs.as_array()) .map(|arr| { let v: Vec = arr - .into_iter() + .iter() .filter_map(|s| s.as_str().map(|st| st.to_string())) .collect(); v @@ -464,7 +464,7 @@ impl SensuClient { println!("Active silences:"); let resp = self.request(Method::GET, SensuEndpoint::Silenced, None)?; if let Some(Value::Array(v)) = resp { - if v.len() == 0 { + if v.is_empty() { println!("\tNo silences"); return Ok(()); } diff --git a/sensu/src/payload.rs b/sensu/src/payload.rs index a1c1030..3a4cf35 100644 --- a/sensu/src/payload.rs +++ b/sensu/src/payload.rs @@ -20,7 +20,7 @@ impl Into> for SensuPayload { let mut payload = Map::new(); // Always inject USER information into payload as creator field - let user = env::var("USER").unwrap_or("shush".to_string()); + let user = env::var("USER").unwrap_or_else(|_| "shush".to_string()); payload.insert("creator".to_string(), Value::String(user)); // Handle subscription for payload as Sensu client value, subscription, or all diff --git a/sensu/src/resources.rs b/sensu/src/resources.rs index 4fc126f..ed460e0 100644 --- a/sensu/src/resources.rs +++ b/sensu/src/resources.rs @@ -35,7 +35,7 @@ impl Display for ShushResources { ShushResourceType::Client => write!(f, "Sensu clients: ")?, ShushResourceType::Sub => write!(f, "Subscriptions: ")?, }; - write!(f, "{}", if self.resources.len() > 0 { + write!(f, "{}", if !self.resources.is_empty() { self.resources.join(", ") } else { return Err(fmt::Error); diff --git a/src/config.rs b/src/config.rs index 0253c58..153a598 100644 --- a/src/config.rs +++ b/src/config.rs @@ -64,7 +64,7 @@ fn sub_or_rest(input: &str) -> IResult<&str, String> { fn substitute_vars(input: &str) -> IResult<&str, String> { let (mut i, mut o) = sub_or_rest(input)?; - while i.len() > 0 { + while !i.is_empty() { let (loop_i, loop_o) = sub_or_rest(i)?; i = loop_i; o += &loop_o; diff --git a/src/main.rs b/src/main.rs index c478831..dacb812 100644 --- a/src/main.rs +++ b/src/main.rs @@ -152,7 +152,7 @@ pub fn main() -> Result<(), Box> { let shush_opts = shush_args.getopts(); let shush_cfg = shush_args.getconf(); - let mut client = SensuClient::new(shush_cfg.get("api").unwrap_or(String::new()))?; + let mut client = SensuClient::new(shush_cfg.get("api").unwrap_or_default())?; match shush_opts { ShushOpts::Silence(s) => client.silence(s)?, diff --git a/src/opts.rs b/src/opts.rs index 0d67efc..bd806b4 100644 --- a/src/opts.rs +++ b/src/opts.rs @@ -108,11 +108,11 @@ impl<'a> Args<'a> { let clearopts = |matches: &ArgMatches, res_arg, res_type| { ShushOpts::Clear(ClearOpts { resources: matches.value_of(res_arg).map(|st| ShushResources { - resources: st.split(",").map(|s| s.to_string()).collect(), + resources: st.split(',').map(|s| s.to_string()).collect(), res_type, }), checks: matches.value_of("checks") - .map(|st| st.split(",").map(|s| s.to_string()).collect()), + .map(|st| st.split(',').map(|s| s.to_string()).collect()), }) }; @@ -125,24 +125,24 @@ impl<'a> Args<'a> { let expiration = |matches: &ArgMatches| { get_expiration(matches.value_of("expire").map(|s| s.to_string()) - .unwrap_or("2h".to_string()), + .unwrap_or_else(|| "2h".to_string()), matches.is_present("expireonresolve")) }; let silenceopts = |matches: &ArgMatches, res_arg, res_type| { ShushOpts::Silence(SilenceOpts { resources: matches.value_of(res_arg).map(|st| ShushResources { - resources: st.split(",").map(|s| s.to_string()).collect(), + resources: st.split(',').map(|s| s.to_string()).collect(), res_type, }), - checks: matches.value_of("checks").map(|st| st.split(",") + checks: matches.value_of("checks").map(|st| st.split(',') .map(|s| s.to_string()).collect()), expire: expiration(matches), }) }; let matches = &self.0; - let shush_opts = if matches.is_present("nodes") { + if matches.is_present("nodes") { if matches.is_present("remove") { clearopts(matches, "nodes", ShushResourceType::Node) } else if matches.is_present("list") { @@ -184,8 +184,7 @@ impl<'a> Args<'a> { expire: expiration(matches), }) } - }; - shush_opts + } } pub fn get_match(&self, option: &str) -> Option { @@ -193,6 +192,6 @@ impl<'a> Args<'a> { } pub fn get_match_as_vec(&self, option: &str) -> Option> { - self.0.value_of(option).map(|st| st.split(",").map(|s| s.to_string()).collect()) + self.0.value_of(option).map(|st| st.split(',').map(|s| s.to_string()).collect()) } }