use std::{
collections::HashMap,
error::Error,
ffi::CStr,
fs,
path::PathBuf,
sync::mpsc::{self, SyncSender},
thread,
time::Duration,
};
use chrono::{Datelike, Local, Timelike};
use qmetaobject::{
prelude::*, qml_register_singleton_type, qtcore::core_application::QCoreApplication, QGadget,
QMetaType, QSingletonInit, QUrl, SimpleListItem, SimpleListModel,
};
use icinga_client::{
client::{self, Client, Result},
types::{Host, HostState, IcingaObjectResult, IcingaObjectResults, Service, ServiceState},
};
use istamon::cfg::{self, Cfg, IcingaApiServer, PasswordManager, PasswordSource};
use istamon::kwallet;
use istamon_qml_extras::qwindow::QWindowPointer;
use structopt::StructOpt;
use url::Url;
#[macro_use]
extern crate cstr;
#[derive(QObject, Default)]
#[allow(non_snake_case)]
struct IstamonUtil {
base: qt_base_class!(trait QObject),
urlFileName: qt_method!(fn(&self, url: QUrl) -> QString),
}
#[allow(non_snake_case)]
impl IstamonUtil {
fn urlFileName(&self, qurl: QUrl) -> QString {
let url: Option<Url> = QString::from(qurl).to_string().parse().ok();
url.and_then(|url| url.to_file_path().ok())
.map(|p| QString::from(&p.to_string_lossy() as &str))
.unwrap_or_default()
}
}
impl QSingletonInit for IstamonUtil {
fn init(&mut self) {
}
}
#[derive(QObject, Default)]
struct IstamonContainer {
thread: Option<SyncSender<()>>,
base: qt_base_class!(trait QObject),
get_role: qt_method!(fn(&self, role_name: QString) -> QVariant),
last_run: qt_property!(QDateTime; NOTIFY last_run_changed),
last_run_changed: qt_signal!(),
critical_count: qt_property!(usize; NOTIFY critical_count_changed),
critical_count_changed: qt_signal!(),
critical_handled_count: qt_property!(usize; NOTIFY critical_handled_count_changed),
critical_handled_count_changed: qt_signal!(),
warning_count: qt_property!(usize; NOTIFY warning_count_changed),
warning_count_changed: qt_signal!(),
warning_handled_count: qt_property!(usize; NOTIFY warning_handled_count_changed),
warning_handled_count_changed: qt_signal!(),
unknown_count: qt_property!(usize; NOTIFY unknown_count_changed),
unknown_count_changed: qt_signal!(),
unknown_handled_count: qt_property!(usize; NOTIFY unknown_handled_count_changed),
unknown_handled_count_changed: qt_signal!(),
error: qt_property!(bool; NOTIFY error_changed),
error_changed: qt_signal!(),
host_down_count: qt_property!(usize; NOTIFY host_down_changed),
host_down_changed: qt_signal!(),
host_down_handled_count: qt_property!(usize; NOTIFY host_down_handled_changed),
host_down_handled_changed: qt_signal!(),
list: qt_property!(QPointer<SimpleListModel<IstamonListItem>>; NOTIFY list_changed WRITE set_simple_list_model),
list_changed: qt_signal!(),
status_icon_name: qt_property!(QString; NOTIFY status_icon_name_changed),
status_icon_name_changed: qt_signal!(),
item_count: usize,
role_indices: Vec<(QByteArray, i32)>,
}
impl IstamonContainer {
fn set_last_run_now(&mut self) {
let now = Local::now();
let date = QDate::from_y_m_d(now.year_ce().1 as i32, now.month() as i32, now.day() as i32);
let time = QTime::from_h_m_s_ms(
now.hour() as i32,
now.minute() as i32,
Some(now.second() as i32),
None,
);
self.last_run = QDateTime::from_date_time_local_timezone(date, time);
self.last_run_changed();
}
fn set_counts(&mut self, new_items: &[Service], hosts: &[Host]) {
self.item_count = new_items.len();
self.critical_count = new_items
.iter()
.filter(|i| i.state == ServiceState::CRITICAL)
.count();
self.critical_handled_count = new_items
.iter()
.filter(|i| i.state == ServiceState::CRITICAL && i.is_handled())
.count();
self.warning_count = new_items
.iter()
.filter(|i| i.state == ServiceState::WARNING)
.count();
self.warning_handled_count = new_items
.iter()
.filter(|i| i.state == ServiceState::WARNING && i.is_handled())
.count();
self.unknown_count = new_items
.iter()
.filter(|i| i.state == ServiceState::UNKNOWN)
.count();
self.unknown_handled_count = new_items
.iter()
.filter(|i| i.state == ServiceState::UNKNOWN && i.is_handled())
.count();
self.host_down_count = hosts.iter().filter(|h| h.state == HostState::DOWN).count();
self.host_down_handled_count = hosts
.iter()
.filter(|h| h.state == HostState::DOWN && h.is_handled())
.count();
self.critical_count_changed();
self.warning_count_changed();
self.unknown_count_changed();
self.critical_handled_count_changed();
self.warning_handled_count_changed();
self.unknown_handled_count_changed();
self.host_down_changed();
self.host_down_handled_changed();
}
fn set_new_items(&mut self, mut services: Vec<Service>, mut hosts: Vec<Host>) {
hosts.sort_by(|i1, i2| i1.state.cmp(&i2.state).reverse());
services.sort_by(|i1, i2| i1.state.cmp(&i2.state).reverse());
let new_items = hosts
.into_iter()
.map(IstamonListItem::from)
.chain(services.into_iter().map(IstamonListItem::from))
.collect();
self.list
.as_pinned()
.map(|l| l.borrow_mut().reset_data(new_items));
}
fn set_error(&mut self, error: bool) {
self.error = error;
self.error_changed();
}
fn set_status_icon_name(&mut self) {
let icon_name = if self.error {
"istamon-icon-error.svg"
} else if self.host_down_count - self.host_down_handled_count > 0
|| self.critical_count - self.critical_handled_count > 0
{
"istamon-icon-critical.svg"
} else if self.warning_count - self.warning_handled_count > 0 {
"istamon-icon-warning.svg"
} else if self.unknown_count - self.unknown_handled_count > 0 {
"istamon-icon-unknown.svg"
} else {
"istamon-icon-ok.svg"
};
let icon_name: QString = icon_name.into();
if icon_name != self.status_icon_name {
self.status_icon_name = icon_name.into();
self.status_icon_name_changed();
}
}
fn set_new_result(&mut self, services: Vec<Service>, hosts: Vec<Host>, error: bool) {
self.set_error(error);
self.set_counts(&services, &hosts);
self.set_new_items(services, hosts);
self.set_last_run_now();
self.set_status_icon_name();
}
fn set_simple_list_model(&mut self, lm: QPointer<SimpleListModel<IstamonListItem>>) {
if let Some(list) = lm.as_ref() {
self.role_indices = list
.role_names()
.iter()
.map(|entry| (entry.1.to_owned(), entry.0.to_owned()))
.collect();
} else {
self.role_indices = Default::default();
}
self.list = lm;
}
fn get_role(&self, role_name: QString) -> QVariant {
self.role_indices
.iter()
.find(|entry| entry.0 == role_name.to_qvariant().to_qbytearray())
.map(|entry| entry.1.to_qvariant())
.unwrap_or_default()
}
pub fn start(&mut self, cfg: &Cfg) -> std::result::Result<(), Box<dyn Error>> {
let client = new_client(cfg)?;
self.thread.take().map(|s| s.send(()));
let (snd, rcv) = mpsc::sync_channel::<()>(1);
self.thread = Some(snd);
let ptr_upd = QPointer::from(self as &Self);
let upd = qmetaobject::queued_callback(move |new_results: (Vec<Service>, Vec<Host>)| {
let (services, hosts) = new_results;
ptr_upd.as_pinned().map(|self_| {
let mut self_mut = self_.borrow_mut();
self_mut.set_new_result(services, hosts, false);
});
});
let ptr_err = QPointer::from(self as &Self);
let upd_err = qmetaobject::queued_callback(move |_: ()| {
ptr_err.as_pinned().map(|self_| {
let mut self_mut = self_.borrow_mut();
self_mut.set_new_result(vec![], vec![], true);
});
});
upd_err(());
thread::spawn(move || {
while rcv.try_recv().is_err() {
let services_result = get_services(&client);
let hosts_result = get_hosts(&client);
match services_result.and_then(|new_items| Ok((new_items, hosts_result?))) {
Ok(new_results) => {
upd(new_results);
}
Err(e) => {
println!("ERR {}", e);
upd_err(());
}
}
thread::sleep(Duration::new(5, 0));
}
});
Ok(())
}
}
fn new_client(cfg: &Cfg) -> std::result::Result<Client, Box<dyn Error>> {
let client = cfg.clone().into_client()?;
Ok(client)
}
fn get_services(c: &Client) -> Result<Vec<Service>> {
let results: IcingaObjectResults<Service> =
c.send_request(c.request(client::Method::GET, "/v1/objects/services").unwrap())?;
Ok(results.results.into_iter().map(extract_object).collect())
}
fn get_hosts(c: &Client) -> Result<Vec<Host>> {
let results: IcingaObjectResults<Host> =
c.send_request(c.request(client::Method::GET, "/v1/objects/hosts").unwrap())?;
Ok(results.results.into_iter().map(extract_object).collect())
}
fn extract_object<T>(r: IcingaObjectResult<T>) -> T {
r.attrs
}
#[derive(SimpleListItem, Default, Clone)]
#[allow(non_snake_case)]
struct IstamonListItem {
pub id: String,
pub displayName: String,
pub hostName: String,
pub lastCheckOutput: String,
pub itemState: u8,
pub handled: bool,
pub hostReachable: bool,
pub isService: bool,
}
impl From<Host> for IstamonListItem {
fn from(h: Host) -> Self {
IstamonListItem {
id: h.name.clone(),
displayName: h.display_name.clone(),
hostName: h.name.clone(),
lastCheckOutput: String::new(),
itemState: h.state.into(),
handled: h.is_handled(),
hostReachable: h.state == HostState::UP,
isService: false,
}
}
}
impl From<Service> for IstamonListItem {
fn from(s: Service) -> Self {
IstamonListItem {
id: s.name.clone(),
displayName: s.display_name.clone(),
hostName: s.host_name.clone(),
lastCheckOutput: s
.last_check_result
.as_ref()
.map(|c| c.output.clone())
.unwrap_or(String::from("n/a")),
itemState: s.state.into(),
handled: s.is_handled(),
hostReachable: s.host_is_reachable(),
isService: true,
}
}
}
#[derive(QObject, Default)]
struct WindowIcon {
base_class: qt_base_class!(trait QObject),
window: qt_property!(QWindowPointer; WRITE set_window),
icon_name: qt_property!(QString; WRITE set_icon_name NOTIFY icon_name_changed),
icon_name_changed: qt_signal!(),
}
fn icon_path(icon_name: &QString) -> QString {
format!(":/icons/{}", icon_name).into()
}
impl WindowIcon {
fn set_window(&mut self, wp: QWindowPointer) {
self.window = wp;
self.update_icon()
}
fn set_icon_name(&mut self, icon_name: QString) {
self.icon_name = icon_name;
self.update_icon();
self.icon_name_changed();
}
fn update_icon(&self) {
if self.icon_name != Default::default() {
self.window
.as_ref()
.map(|w| w.set_icon(icon_path(&self.icon_name)));
}
}
}
#[derive(SimpleListItem, Default)]
#[allow(non_snake_case)]
struct ServerListItem {
pub serverName: QString,
}
#[derive(QObject)]
#[allow(non_snake_case)]
struct IstamonCfg {
cfg_file: Option<PathBuf>,
cfg: std::result::Result<Cfg, Box<dyn Error>>,
servers: HashMap<String, IcingaApiServer>,
password_manager: Option<Box<dyn PasswordManager>>,
base_class: qt_base_class!(trait QObject),
cfgUpdated: qt_signal!(),
notification: qt_signal!(msg: QString),
showConfigErrors: qt_method!(fn(&self) -> ()),
isConfigured: qt_property!(bool; READ is_configured NOTIFY cfgUpdated),
title: qt_property!(QString; NOTIFY cfgUpdated READ title),
hasPasswordManager: qt_method!(
fn hasPasswordManager(&self) -> bool {
self.password_manager.is_some()
}
),
istamon: qt_property!(QPointer<IstamonContainer>; WRITE set_istamon),
getCfg: qt_method!(
fn getCfg(&self) -> QVariant {
self.get_cfg().to_qvariant()
}
),
setCfg: qt_method!(fn(&mut self, cfg: CfgModel) -> ()),
saveCfg: qt_method!(fn(&mut self) -> ()),
}
macro_rules! log_error {
($s:expr, $f:literal, $($e:expr),+) => {{
{
let msg = format!($f, $($e),+);
eprintln!("ERROR: {}", msg);
$s.notification(format!("ERROR: {}", msg).into());
}
}}
}
macro_rules! log_info {
($s:expr, $f:literal, $($e:expr),+) => {{
{
let msg = format!($f, $($e),+);
eprintln!("INFO: {}", msg);
$s.notification(msg.into());
}
}}
}
impl IstamonCfg {
pub fn set_istamon(&mut self, istamon: QPointer<IstamonContainer>) {
self.istamon = istamon;
if let Err(e) = self.start_istamon() {
log_error!(self, "failed to start client: {}", e);
};
}
fn start_istamon(&self) -> std::result::Result<(), String> {
if let Ok(cfg) = &self.cfg {
self.istamon
.as_pinned()
.map(|c| c.borrow_mut().start(cfg).map_err(|e| e.to_string()))
.unwrap_or(Err("istamon component was deleted".to_string()))
} else {
Ok(())
}
}
pub fn is_configured(&self) -> bool {
self.cfg.is_ok()
}
pub fn title(&self) -> QString {
self.cfg
.as_ref()
.map(|c| {
QString::from(format!(
"{}{}",
c.url().host_str(),
c.url()
.get()
.port()
.map(|p| format!(":{}", p))
.unwrap_or_default()
))
})
.unwrap_or_default()
}
#[allow(non_snake_case)]
fn showConfigErrors(&self) {
if let Err(e) = &self.cfg {
log_error!(self, "Error loading config: {}", e);
}
}
pub fn get_cfg(&self) -> CfgModel {
match &self.cfg {
Ok(cfg) => CfgModel::from(cfg),
Err(e) => {
log_error!(self, "Error loading config: {}", e);
Default::default()
}
}
}
#[allow(non_snake_case)]
pub fn setCfg(&mut self, cfgModel: CfgModel) {
let new_cfg: std::result::Result<Cfg, String> = (|| {
let ca_cert = if cfgModel.hasCaCert {
Some(PathBuf::from(cfgModel.caCert))
} else {
None
};
let url = cfgModel.url.parse()?;
if cfgModel.hasCredentials {
let user = cfgModel.user;
let password = cfgModel.password;
let password_source = match cfgModel.savePasswordChoice.as_str() {
"dontsave" => PasswordSource::None,
"encrypted" => self
.password_manager
.as_ref()
.map(|p| PasswordSource::Encrypted { provider: p.name() })
.unwrap_or(PasswordSource::None),
"unencrypted" => PasswordSource::Unencrypted,
_ => Err(format!(
"Unknown password save choice: {}",
cfgModel.savePasswordChoice
))?,
};
Ok(Cfg::new_with_credentials(
url,
user,
password,
password_source,
ca_cert,
))
} else {
Ok(Cfg::new_without_credentials(url, ca_cert))
}
})();
match new_cfg {
Ok(cfg) => {
self.cfg = Ok(cfg);
match self.start_istamon() {
Ok(()) => self.cfgUpdated(),
Err(e) => log_error!(self, "Error staring client: {}", e),
}
}
Err(e) => log_error!(self, "Error setting config: {}", e),
}
}
#[allow(non_snake_case)]
pub fn saveCfg(&mut self) {
let result: std::result::Result<(), Box<dyn Error>> = (|| {
if let Some(cfg_path) = &self.cfg_file {
if !cfg_path.exists() {
fs::create_dir_all(cfg_path.parent().unwrap())?;
}
let cfg = self.cfg.as_ref().map_err(|e| e.to_string())?;
self.servers
.insert(DEFAULT_CONFIG_ENTRY.to_string(), IcingaApiServer::from(cfg));
cfg::save_api_server_configs(cfg_path, &self.servers)?;
log_info!(
self,
"Successfully saved config to {}",
cfg_path.to_string_lossy()
);
match (cfg.password_source(), cfg.credentials()) {
(PasswordSource::Encrypted { .. }, Some((user, Some(password)))) => {
self.password_manager
.as_ref()
.map(|p| p.store_password(cfg.url().host_str(), user, password))
.unwrap_or(Err(
"No password manager loaded. This is unexpected.".into()
))?;
}
_ => {}
}
} else {
Err("Don't know were to save the config")?
}
Ok(())
})();
if let Err(e) = result {
log_error!(self, "Error saving config: {}", e);
}
}
}
impl Default for IstamonCfg {
#[allow(non_snake_case)]
fn default() -> Self {
let password_manager = kwallet::open();
let cfg_file = cfg::default_config_file();
let cfg_and_servers = (|| {
let opts = Opts::from_args();
let configured_api_servers = cfg::load_api_server_configs_or_empty(cfg_file.clone())
.map_err(|e| e.to_string())?;
Ok((
Cfg::new(
password_manager
.as_ref()
.ok()
.map(|l| l as &dyn PasswordManager),
&configured_api_servers,
opts.url.as_ref().map(|s| s as &str),
DEFAULT_CONFIG_ENTRY,
),
configured_api_servers,
))
})();
let servers = cfg_and_servers
.as_ref()
.map(|v| v.1.clone())
.unwrap_or_default();
let cfg = cfg_and_servers.and_then(|v| v.0);
Self {
cfg_file,
cfg: cfg.map_err(|e| e.into()),
servers,
title: Default::default(),
istamon: Default::default(),
password_manager: password_manager
.ok()
.map::<Box<dyn PasswordManager>, _>(|l| Box::new(l)),
base_class: Default::default(),
cfgUpdated: Default::default(),
isConfigured: Default::default(),
hasPasswordManager: Default::default(),
getCfg: Default::default(),
setCfg: Default::default(),
saveCfg: Default::default(),
notification: Default::default(),
showConfigErrors: Default::default(),
}
}
}
#[derive(Clone, Default, QGadget)]
#[allow(non_snake_case, dead_code)]
struct CfgModel {
url: qt_property!(String),
hasCredentials: qt_property!(bool),
user: qt_property!(String),
password: qt_property!(String),
savePasswordChoice: qt_property!(String),
hasCaCert: qt_property!(bool),
caCert: qt_property!(String),
}
#[allow(non_snake_case)]
impl From<&Cfg> for CfgModel {
fn from(cfg: &Cfg) -> Self {
let credentials = cfg.credentials().clone();
let ca_cert = cfg.ca_cert().clone();
let savePasswordChoice = match cfg.password_source() {
cfg::PasswordSource::None => "dontsave".to_string(),
cfg::PasswordSource::Encrypted { provider: _ } => "encrypted".to_string(),
cfg::PasswordSource::Unencrypted => "unencrypted".to_string(),
};
CfgModel {
url: cfg.url().get().to_string(),
hasCredentials: credentials.is_some(),
user: credentials
.as_ref()
.map(|c| c.0.clone())
.unwrap_or_default(),
password: credentials.and_then(|c| c.1.clone()).unwrap_or_default(),
savePasswordChoice,
hasCaCert: ca_cert.is_some(),
caCert: ca_cert
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default(),
}
}
}
#[derive(StructOpt)]
struct Opts {
url: Option<String>,
}
const QML_URI: &'static CStr = cstr!("istamon.internal");
const QML_MAJOR_VERSION: u32 = 1;
const QML_MINOR_VERSION: u32 = 0;
const DEFAULT_CONFIG_ENTRY: &'static str = "default";
fn main() {
qrc!(register_icons,
"icons" {
"istamon-icon.svg",
"istamon-icon-ok.svg",
"istamon-icon-warning.svg",
"istamon-icon-critical.svg",
"istamon-icon-error.svg",
"istamon-icon-unknown.svg",
});
register_icons();
#[cfg(not(feature = "load-qml-from-cwd"))]
let main_qml = {
qrc!(register_qml_files,
"qml" {
"main.qml",
});
register_qml_files();
":/qml/main.qml"
};
#[cfg(feature = "load-qml-from-cwd")]
let main_qml = "./main.qml";
qml_register_type::<IstamonCfg>(
QML_URI,
QML_MAJOR_VERSION,
QML_MINOR_VERSION,
cstr!("IstamonCfg"),
);
qml_register_type::<IstamonContainer>(
QML_URI,
QML_MAJOR_VERSION,
QML_MINOR_VERSION,
cstr!("IstamonContainer"),
);
qml_register_type::<SimpleListModel<IstamonListItem>>(
QML_URI,
QML_MAJOR_VERSION,
QML_MINOR_VERSION,
cstr!("IstamonListModel"),
);
qml_register_type::<WindowIcon>(
QML_URI,
QML_MAJOR_VERSION,
QML_MINOR_VERSION,
cstr!("WindowIcon"),
);
qml_register_singleton_type::<IstamonUtil>(
QML_URI,
QML_MAJOR_VERSION,
QML_MINOR_VERSION,
cstr!("IstamonUtil"),
);
QCoreApplication::set_organization_name("lu-fennell".into());
QCoreApplication::set_organization_domain("de.lu-fennell".into());
let mut engine = QmlEngine::new();
engine.load_file(main_qml.into());
engine.exec();
}