icinga2-api 0.2.21

Rust client for the Icinga2 API
Documentation
//! Main API object (blocking version)

use std::{path::Path, str::from_utf8};

use serde::{de::DeserializeOwned, Serialize};

use crate::{
    config::Icinga2Instance,
    types::rest::{RestApiEndpoint, RestApiResponse},
};

/// the runtime object for an Icinga2 instance (blocking variant)
#[derive(Debug, Clone)]
pub struct Icinga2 {
    /// the HTTP client to use
    client: reqwest::blocking::Client,
    /// the base URL for the Icinga API
    pub url: url::Url,
    /// username
    pub username: String,
    /// password
    password: String,
}

impl Icinga2 {
    /// create a new Icinga2 instance from a config that was
    /// either manually created or previously loaded via [Icinga2Instance::from_config_file]
    ///
    /// # Errors
    /// this fails if the CA certificate file mentioned in the configuration
    /// can not be found or parsed
    pub fn from_instance_config(config: &Icinga2Instance) -> Result<Self, crate::error::Error> {
        let client_builder = reqwest::blocking::ClientBuilder::new();
        let client_builder = client_builder.user_agent(concat!(
            env!("CARGO_PKG_NAME"),
            "/",
            env!("CARGO_PKG_VERSION")
        ));
        let mut headers = reqwest::header::HeaderMap::new();
        headers.insert(
            "Content-Type",
            reqwest::header::HeaderValue::from_static("application/json"),
        );
        headers.insert(
            "Accept",
            reqwest::header::HeaderValue::from_static("application/json"),
        );
        let client_builder = client_builder.default_headers(headers);
        let client_builder = if let Some(ca_certificate) = &config.ca_certificate {
            let ca_cert_content = std::fs::read(ca_certificate)
                .map_err(crate::error::Error::CouldNotReadCACertFile)?;
            let ca_cert = reqwest::Certificate::from_pem(&ca_cert_content)
                .map_err(crate::error::Error::CouldNotParsePEMCACertificate)?;
            client_builder.tls_certs_only([ca_cert])
        } else {
            client_builder
        };
        let client = client_builder
            .build()
            .map_err(crate::error::Error::CouldNotBuildReqwestClientFromSuppliedInformation)?;
        let url =
            url::Url::parse(&config.url).map_err(crate::error::Error::CouldNotParseUrlInConfig)?;
        let username = config.username.clone();
        let password = config.password.clone();
        Ok(Icinga2 {
            client,
            url,
            username,
            password,
        })
    }

    /// create a new Icinga2 instance from a TOML config file
    ///
    /// # Errors
    /// this fails if the configuration file can not be found or parsed
    /// or the CA certificate file mentioned in the configuration file
    /// can not be found or parsed
    pub fn from_config_file(path: &Path) -> Result<Self, crate::error::Error> {
        let icinga_instance = Icinga2Instance::from_config_file(path)?;
        Self::from_instance_config(&icinga_instance)
    }

    /// common code for the REST API calls
    ///
    /// # Errors
    ///
    /// this returns an error if encoding, the actual request, or decoding of the response fail
    pub fn rest<ApiEndpoint, Res>(
        &self,
        api_endpoint: ApiEndpoint,
    ) -> Result<Res, crate::error::Error>
    where
        ApiEndpoint: RestApiEndpoint,
        <ApiEndpoint as RestApiEndpoint>::RequestBody: Clone + Serialize + std::fmt::Debug,
        Res: DeserializeOwned + std::fmt::Debug + RestApiResponse<ApiEndpoint>,
    {
        let method = api_endpoint.method()?;
        let url = api_endpoint.url(&self.url)?;
        let request_body: Option<std::borrow::Cow<<ApiEndpoint as RestApiEndpoint>::RequestBody>> =
            api_endpoint.request_body()?;
        let actual_method = if method == reqwest::Method::GET && request_body.is_some() {
            reqwest::Method::POST
        } else {
            method.to_owned()
        };
        let mut req = self.client.request(actual_method, url.to_owned());
        if method == reqwest::Method::GET && request_body.is_some() {
            tracing::trace!("Sending GET request with body as POST via X-HTTP-Method-Override");
            req = req.header(
                "X-HTTP-Method-Override",
                reqwest::header::HeaderValue::from_static("GET"),
            );
        }
        req = req.basic_auth(&self.username, Some(&self.password));
        if let Some(request_body) = request_body {
            tracing::trace!("Request body:\n{:#?}", request_body);
            req = req.json(&request_body);
        }
        let result = req.send();
        if let Err(ref e) = result {
            tracing::error!(%url, %method, "Icinga2 send error: {:?}", e);
        }
        let result = result?;
        let status = result.status();
        let response_body = result.bytes()?;
        match from_utf8(&response_body) {
            Ok(response_body) => {
                tracing::trace!("Response body:\n{}", &response_body);
            }
            Err(e) => {
                tracing::trace!(
                    "Response body that could not be parsed as utf8 because of {}:\n{:?}",
                    &e,
                    &response_body
                );
            }
        }
        if status.is_client_error() {
            tracing::error!(%url, %method, "Icinga2 status error (client error): {:?}", status);
        } else if status.is_server_error() {
            tracing::error!(%url, %method, "Icinga2 status error (server error): {:?}", status);
        }
        if response_body.is_empty() {
            Err(crate::error::Error::EmptyResponseBody(status))
        } else {
            let jd = &mut serde_json::Deserializer::from_slice(&response_body);
            match serde_path_to_error::deserialize(jd) {
                Ok(response_body) => {
                    tracing::trace!("Parsed response body:\n{:#?}", response_body);
                    Ok(response_body)
                }
                Err(e) => {
                    let path = e.path();
                    tracing::error!("Parsing failed at path {}: {}", path.to_string(), e.inner());
                    if let Ok(response_body) = serde_json::from_slice(&response_body) {
                        let mut response_body: serde_json::Value = response_body;
                        for segment in path {
                            match (response_body, segment) {
                                (
                                    serde_json::Value::Array(vs),
                                    serde_path_to_error::Segment::Seq { index },
                                ) => {
                                    if let Some(v) = vs.get(*index) {
                                        response_body = v.to_owned();
                                    } else {
                                        // if we can not find the element serde_path_to_error references fall back to just returning the error
                                        return Err(e.into());
                                    }
                                }
                                (
                                    serde_json::Value::Object(m),
                                    serde_path_to_error::Segment::Map { key },
                                ) => {
                                    if let Some(v) = m.get(key) {
                                        response_body = v.to_owned();
                                    } else {
                                        // if we can not find the element serde_path_to_error references fall back to just returning the error
                                        return Err(e.into());
                                    }
                                }
                                _ => {
                                    // if we can not find the element serde_path_to_error references fall back to just returning the error
                                    return Err(e.into());
                                }
                            }
                        }
                        tracing::error!("Value in location path references is: {}", response_body);
                    }
                    Err(e.into())
                }
            }
        }
    }
}