mod cli;
use cli::{AckTarget, ClientCommand, Command, KWalletCommand};
use icinga_client::client::Client;
use icinga_client::client::{self, header};
use icinga_client::types::HostState;
use icinga_client::types::Service;
use icinga_client::types::{AckState, Host};
use icinga_client::types::{Acknowledgement, IcingaObjectResults};
use istamon::cfg::{self, Cfg, PasswordManager};
use istamon::kwallet;
use serde_json::Value;
use status::Status;
use structopt::clap::App;
use std::io::{self, Read};
type MainResult<T> = Result<T, Box<dyn std::error::Error>>;
const PROGRAM_NAME: &'static str = env!("CARGO_PKG_NAME");
const DESCRIPTION: &'static str = env!("CARGO_PKG_DESCRIPTION");
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
const AUTHOR: &'static str = env!("CARGO_PKG_AUTHORS");
fn main() -> MainResult<()> {
let options = cli::parse_args(
App::new(PROGRAM_NAME)
.author(AUTHOR)
.about(DESCRIPTION)
.version(VERSION),
)?;
let config_file = cfg::default_config_file();
let configured_api_servers = cfg::load_api_server_configs_or_empty(config_file)?;
let loader = kwallet::open();
let cfg: Cfg = Cfg::new(
loader.as_ref().ok().map(|m| m as &dyn PasswordManager),
&configured_api_servers,
options.url_string.as_ref().map(|s| s as &str),
"default",
)?;
match options.command {
Command::Client(client_command) => {
let client = cfg.into_client()?;
match client_command {
ClientCommand::RestApi {
method,
read_body,
path,
} => {
let body = if read_body {
Some(read_body_from_stdin()?)
} else {
None
};
rest_api(&client, method, &path, body)
}
ClientCommand::Status { verbose } => status(&client, verbose),
ClientCommand::Ack {
target,
until,
comment,
} => ack(
&client,
target,
until,
&comment.unwrap_or("<n/a>".to_owned()),
),
ClientCommand::HostNotification { host, comment } => host_notification(
&client,
"/v1/actions/send-custom-notification",
&host,
&comment,
),
}
}
Command::KWallet(kwallet_command) => {
let user = &cfg
.credentials()
.ok_or(format!("No user specified for {}", cfg.url().get()))?
.0;
let host = cfg
.url()
.get()
.host()
.ok_or(format!("No host specified for {}", cfg.url().get()))?
.to_string();
match kwallet_command {
KWalletCommand::WritePassword => {
let password = rpassword::read_password_from_tty(Some(&format!(
"Password for {}@{}: ",
&user, &host
)))?;
let loader = loader?;
loader.create_folder()?;
loader.write_password(&format!("{}@{}", &user, &host), &password)?;
Ok(())
}
KWalletCommand::ReadPassword => {
let password: String = loader?
.load_password(&host, &user)?
.ok_or("No password found")?;
println!("{}", password);
Ok(())
}
KWalletCommand::RemovePassword => {
loader?.remove_entry(&format!("{}@{}", &user, &host))?;
Ok(())
}
}
}
}
}
fn read_body_from_stdin() -> MainResult<String> {
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)?;
Ok(buffer)
}
fn rest_api(
client: &Client,
method: cli::Method,
path: &str,
request_body: Option<String>,
) -> MainResult<()> {
let method = match method {
cli::Method::Get => client::Method::GET,
cli::Method::Post => client::Method::POST,
};
let req = client.request(method, path)?;
let req = if let Some(body_string) = request_body {
req.body(body_string)
.header(header::CONTENT_TYPE, "application/json")
} else {
req
};
let body = client.send_request::<Value>(req)?;
println!("{}", serde_json::to_string_pretty(&body)?);
Ok(())
}
fn host_notification(client: &Client, path: &str, host: &str, comment: &str) -> MainResult<()> {
let filter = format!(r#"host.name == "{}""#, host);
let req_body: Value = serde_json::json!({
"type": "Host",
"author": "icinga-client",
"comment": comment,
"filter": filter
});
println!("Trying: {}", req_body.to_string());
let resp_body = client.send_request(
client
.request(client::Method::POST, path)?
.body(req_body.to_string()),
)?;
println!("{}", serde_json::to_string_pretty(&resp_body)?);
Ok(())
}
fn ack(
client: &Client,
ack_target: AckTarget,
until: Option<u64>,
comment: &str,
) -> MainResult<()> {
let query = ack_target.query();
let mut req_body: Value = serde_json::json!({
"type": ack_target.object_type(),
"author": "icingaadmin",
"comment": comment,
});
if let Some(expiry) = until {
req_body
.as_object_mut()
.unwrap()
.insert("expiry".to_owned(), serde_json::json!(expiry));
}
let resp_body: Value = client.send_request(
client
.request(client::Method::POST, "/v1/actions/acknowledge-problem")?
.query(&query)
.body(req_body.to_string()),
)?;
println!("{}", serde_json::to_string_pretty(&resp_body)?);
Ok(())
}
fn status(sender: &Client, verbose: bool) -> MainResult<()> {
let hosts = request_results(&sender, "/v1/objects/hosts")?;
let services: Vec<Service> = request_results(&sender, "/v1/objects/services")?;
let status = status::Status::new(&hosts, &services);
if verbose {
print_status_verbose(&status);
} else {
print_status(&status);
}
Ok(())
}
fn print_status_verbose(status: &Status) {
for host in status.hosts() {
print_host(host);
for service in status.services_of(&host.name) {
print_service(service);
}
println!()
}
}
fn print_status(status: &Status) {
let hostnames_up_and_well = status
.hosts()
.filter(|h| status.host_is_up_and_well(h))
.map(|h| h.name.as_str())
.collect::<Vec<_>>();
println!(
"{} hosts are up and well: {:?}",
hostnames_up_and_well.len(),
hostnames_up_and_well
);
for host in status
.hosts()
.filter(|h| h.last_hard_state == HostState::DOWN)
{
print_host(host);
}
println!();
for (host, services_not_ok) in status.hosts_with_service_problems() {
println!(
"* {} is UP, {} of {} services have problems:",
host.name,
services_not_ok.len(),
status.services_of(&host.name).count()
);
for host_service in services_not_ok {
print_service(host_service);
}
println!();
}
}
static TIMEFORMAT: &'static str = "%Y-%m-%d %H:%M:%S";
fn print_host(host: &Host) {
let status: &str = if host.last_hard_state == HostState::UP {
"UP"
} else {
"DOWN"
};
let ack_suffix = ack_suffix(&host.acknowledgement);
println!(
"* {} is {} since {}{}, next check: {}",
host.name,
status,
host.last_state_change.localtime().format(TIMEFORMAT),
ack_suffix,
host.next_check.localtime().format(TIMEFORMAT)
);
}
fn ack_suffix(acknowledgement: &Acknowledgement) -> String {
match acknowledgement.state {
AckState::Acknowledged {
kind: _,
expiry: Some(date),
} => format!(
" (acknowledged until {})",
date.localtime().format(TIMEFORMAT)
),
AckState::Acknowledged {
kind: _,
expiry: None,
} => " (acknowledged)".to_owned(),
AckState::None => "".to_owned(),
}
}
fn print_service(service: &Service) {
let output = service
.last_check_result
.as_ref()
.map_or("<n/a>".to_owned(), |c| c.output.to_owned());
let ack_suffix = ack_suffix(&service.acknowledgement);
println!(
" * {} is {:?}{}",
service.name, service.last_hard_state, ack_suffix
);
println!(
" since: {}",
service.last_state_change.localtime().format(TIMEFORMAT)
);
println!(
" next check: {}",
service.next_check.localtime().format(TIMEFORMAT)
);
println!(" output: {}", output);
}
fn request_results<T: serde::de::DeserializeOwned>(
client: &Client,
path: &str,
) -> MainResult<Vec<T>> {
let result: IcingaObjectResults<T> =
client.send_request(client.request(client::Method::GET, path)?)?;
let result_vec = result.results;
Ok(result_vec.into_iter().map(|r| r.attrs).collect())
}
#[cfg(test)]
mod test {
use super::*;
#[cfg(test)]
use icinga_mock::Session;
#[test]
fn test_request_results() {
Session::new().with_client(|client| {
let hosts: Vec<Host> = request_results(client, "/v1/objects/hosts").unwrap();
assert!(
hosts.len() == 4,
"Expected 4 hosts, got {}: {:#?}",
hosts.len(),
hosts.iter().map(|h| &h.name).collect::<Vec<_>>()
);
})
}
}
mod status {
use std::collections::BTreeSet;
use icinga_client::types::HostState;
use icinga_client::types::ServiceState;
use icinga_client::types::{Host, Service};
pub(crate) struct Status<'a> {
hosts: &'a Vec<Host>,
services: &'a Vec<Service>,
hostnames_up_and_well: BTreeSet<&'a str>,
hostnames_down: BTreeSet<&'a str>,
hostnames_service_not_ok: BTreeSet<&'a str>,
servicenames_not_ok: BTreeSet<&'a str>,
}
impl<'a> Status<'a> {
pub fn new(hosts: &'a Vec<Host>, services: &'a Vec<Service>) -> Self {
let servicenames_not_ok: BTreeSet<&'a str> = services
.iter()
.filter(|s| {
s.last_hard_state != ServiceState::OK && !s.acknowledgement.is_acknowledged()
})
.map(|s| s.name.as_str())
.collect();
let hostnames_up: BTreeSet<&str> = hosts
.iter()
.filter(|h| h.last_hard_state == HostState::UP)
.map(|h| h.name.as_str())
.collect();
let hostnames_service_not_ok: BTreeSet<&str> = services
.iter()
.filter(|s| servicenames_not_ok.contains(s.name.as_str()))
.map(|s| s.host_name.as_str())
.collect();
let hostnames_up_and_well: BTreeSet<&str> = hostnames_up
.difference(&hostnames_service_not_ok)
.cloned()
.collect();
let hostnames_down: BTreeSet<&str> = hosts
.iter()
.map(|h| h.name.as_str())
.filter(|n| !hostnames_up.contains(n))
.collect();
Status {
hosts,
services,
hostnames_up_and_well,
hostnames_down,
hostnames_service_not_ok,
servicenames_not_ok,
}
}
pub fn hosts(&self) -> impl Iterator<Item = &Host> {
self.hosts.iter()
}
pub fn host_is_up_and_well(&self, host: &Host) -> bool {
self.hostnames_up_and_well.contains(host.name.as_str())
}
pub fn services_of(&self, hostname: &str) -> impl Iterator<Item = &Service> {
let host_servicenames: BTreeSet<&str> = self
.services
.iter()
.filter(|s| s.host_name == hostname)
.map(|s| s.name.as_str())
.collect();
self.services
.iter()
.filter(move |s| host_servicenames.contains(s.name.as_str()))
}
fn services_not_ok(&self, hostname: &str) -> impl Iterator<Item = &Service> {
self.services_of(hostname)
.filter(move |s| self.servicenames_not_ok.contains(s.name.as_str()))
}
pub fn hosts_with_service_problems(
&'a self,
) -> impl Iterator<Item = (&'a Host, Vec<&'a Service>)> {
self.hosts
.iter()
.filter(move |h| {
self.hostnames_service_not_ok
.difference(&self.hostnames_down)
.cloned()
.collect::<BTreeSet<_>>()
.contains(h.name.as_str())
})
.map(move |h| (h, self.services_not_ok(&h.name).collect()))
}
}
#[cfg(test)]
mod test {
use icinga_client::types::{Acknowledgement, LastCheckResult, Timestamp};
use super::*;
fn test_host(name: &str, last_hard_state: HostState) -> Host {
Host {
name: String::from(name),
display_name: String::from(name),
address: String::new(),
address6: String::new(),
state: last_hard_state,
last_state: last_hard_state,
last_hard_state,
last_check_result: Some(LastCheckResult {
output: String::from("this is test output"),
}),
last_state_change: Timestamp::new(-1_f64),
last_state_up: Timestamp::new(-1_f64),
last_state_down: Timestamp::new(-1_f64),
next_check: Timestamp::new(-1_f64),
acknowledgement: Acknowledgement::none(),
handled: last_hard_state == HostState::UP,
}
}
fn test_service(
name: &str,
display_name: &str,
hostname: &str,
last_hard_state: ServiceState,
) -> Service {
Service {
name: String::from(name),
display_name: display_name.to_string(),
host_name: String::from(hostname),
last_hard_state,
state: last_hard_state,
last_state: last_hard_state,
last_state_change: Timestamp::new(-1_f64),
last_state_critical: Timestamp::new(-1_f64),
last_state_warning: Timestamp::new(-1_f64),
last_state_ok: Timestamp::new(-1_f64),
last_state_unknown: Timestamp::new(-1_f64),
next_check: Timestamp::new(1_f64),
last_check_result: Some(LastCheckResult {
output: String::from("this is test output"),
}),
acknowledgement: Acknowledgement::none(),
handled: last_hard_state == ServiceState::OK,
last_reachable: true,
}
}
#[test]
fn test_services_of() {
let h1 = test_host("h1", HostState::UP);
let s11 = test_service("s11", "d11", "h1", ServiceState::CRITICAL);
let s12 = test_service("s12", "d12", "h1", ServiceState::OK);
let h2 = test_host("h2", HostState::DOWN);
let s21 = test_service("s21", "d21", "h2", ServiceState::WARNING);
let s3 = test_service("s3", "d3", "h_na", ServiceState::WARNING);
let hosts = vec![h1, h2];
let services = vec![s11.clone(), s12.clone(), s21.clone(), s3.clone()];
let status = Status::new(&hosts, &services);
assert_eq!(
status.services_of("h1").collect::<Vec<_>>(),
vec![&s11, &s12]
);
assert_eq!(status.services_of("h2").collect::<Vec<_>>(), vec![&s21]);
}
}
}