icinga2_api/api/
blocking.rs

1//! Main API object (blocking version)
2
3use std::{path::Path, str::from_utf8};
4
5use serde::{de::DeserializeOwned, Serialize};
6
7use crate::{
8    config::Icinga2Instance,
9    types::rest::{RestApiEndpoint, RestApiResponse},
10};
11
12/// the runtime object for an Icinga2 instance (blocking variant)
13#[derive(Debug, Clone)]
14pub struct Icinga2 {
15    /// the HTTP client to use
16    client: reqwest::blocking::Client,
17    /// the base URL for the Icinga API
18    pub url: url::Url,
19    /// username
20    pub username: String,
21    /// password
22    password: String,
23}
24
25impl Icinga2 {
26    /// create a new Icinga2 instance from a config that was
27    /// either manually created or previously loaded via [Icinga2Instance::from_config_file]
28    ///
29    /// # Errors
30    /// this fails if the CA certificate file mentioned in the configuration
31    /// can not be found or parsed
32    pub fn from_instance_config(config: &Icinga2Instance) -> Result<Self, crate::error::Error> {
33        let client_builder = reqwest::blocking::ClientBuilder::new();
34        let client_builder = client_builder.user_agent(concat!(
35            env!("CARGO_PKG_NAME"),
36            "/",
37            env!("CARGO_PKG_VERSION")
38        ));
39        let mut headers = reqwest::header::HeaderMap::new();
40        headers.insert(
41            "Content-Type",
42            reqwest::header::HeaderValue::from_static("application/json"),
43        );
44        headers.insert(
45            "Accept",
46            reqwest::header::HeaderValue::from_static("application/json"),
47        );
48        let client_builder = client_builder.default_headers(headers);
49        let client_builder = if let Some(ca_certificate) = &config.ca_certificate {
50            let ca_cert_content = std::fs::read(ca_certificate)
51                .map_err(crate::error::Error::CouldNotReadCACertFile)?;
52            let ca_cert = reqwest::Certificate::from_pem(&ca_cert_content)
53                .map_err(crate::error::Error::CouldNotParsePEMCACertificate)?;
54            let client_builder = client_builder.add_root_certificate(ca_cert);
55            client_builder.tls_built_in_root_certs(false)
56        } else {
57            client_builder
58        };
59        let client = client_builder
60            .build()
61            .map_err(crate::error::Error::CouldNotBuildReqwestClientFromSuppliedInformation)?;
62        let url =
63            url::Url::parse(&config.url).map_err(crate::error::Error::CouldNotParseUrlInConfig)?;
64        let username = config.username.clone();
65        let password = config.password.clone();
66        Ok(Icinga2 {
67            client,
68            url,
69            username,
70            password,
71        })
72    }
73
74    /// create a new Icinga2 instance from a TOML config file
75    ///
76    /// # Errors
77    /// this fails if the configuration file can not be found or parsed
78    /// or the CA certificate file mentioned in the configuration file
79    /// can not be found or parsed
80    pub fn from_config_file(path: &Path) -> Result<Self, crate::error::Error> {
81        let icinga_instance = Icinga2Instance::from_config_file(path)?;
82        Self::from_instance_config(&icinga_instance)
83    }
84
85    /// common code for the REST API calls
86    ///
87    /// # Errors
88    ///
89    /// this returns an error if encoding, the actual request, or decoding of the response fail
90    pub fn rest<ApiEndpoint, Res>(
91        &self,
92        api_endpoint: ApiEndpoint,
93    ) -> Result<Res, crate::error::Error>
94    where
95        ApiEndpoint: RestApiEndpoint,
96        <ApiEndpoint as RestApiEndpoint>::RequestBody: Clone + Serialize + std::fmt::Debug,
97        Res: DeserializeOwned + std::fmt::Debug + RestApiResponse<ApiEndpoint>,
98    {
99        let method = api_endpoint.method()?;
100        let url = api_endpoint.url(&self.url)?;
101        let request_body: Option<std::borrow::Cow<<ApiEndpoint as RestApiEndpoint>::RequestBody>> =
102            api_endpoint.request_body()?;
103        let actual_method = if method == reqwest::Method::GET && request_body.is_some() {
104            reqwest::Method::POST
105        } else {
106            method.to_owned()
107        };
108        let mut req = self.client.request(actual_method, url.to_owned());
109        if method == reqwest::Method::GET && request_body.is_some() {
110            tracing::trace!("Sending GET request with body as POST via X-HTTP-Method-Override");
111            req = req.header(
112                "X-HTTP-Method-Override",
113                reqwest::header::HeaderValue::from_static("GET"),
114            );
115        }
116        req = req.basic_auth(&self.username, Some(&self.password));
117        if let Some(request_body) = request_body {
118            tracing::trace!("Request body:\n{:#?}", request_body);
119            req = req.json(&request_body);
120        }
121        let result = req.send();
122        if let Err(ref e) = result {
123            tracing::error!(%url, %method, "Icinga2 send error: {:?}", e);
124        }
125        let result = result?;
126        let status = result.status();
127        let response_body = result.bytes()?;
128        match from_utf8(&response_body) {
129            Ok(response_body) => {
130                tracing::trace!("Response body:\n{}", &response_body);
131            }
132            Err(e) => {
133                tracing::trace!(
134                    "Response body that could not be parsed as utf8 because of {}:\n{:?}",
135                    &e,
136                    &response_body
137                );
138            }
139        }
140        if status.is_client_error() {
141            tracing::error!(%url, %method, "Icinga2 status error (client error): {:?}", status);
142        } else if status.is_server_error() {
143            tracing::error!(%url, %method, "Icinga2 status error (server error): {:?}", status);
144        }
145        if response_body.is_empty() {
146            Err(crate::error::Error::EmptyResponseBody(status))
147        } else {
148            let jd = &mut serde_json::Deserializer::from_slice(&response_body);
149            match serde_path_to_error::deserialize(jd) {
150                Ok(response_body) => {
151                    tracing::trace!("Parsed response body:\n{:#?}", response_body);
152                    Ok(response_body)
153                }
154                Err(e) => {
155                    let path = e.path();
156                    tracing::error!("Parsing failed at path {}: {}", path.to_string(), e.inner());
157                    if let Ok(response_body) = serde_json::from_slice(&response_body) {
158                        let mut response_body: serde_json::Value = response_body;
159                        for segment in path {
160                            match (response_body, segment) {
161                                (
162                                    serde_json::Value::Array(vs),
163                                    serde_path_to_error::Segment::Seq { index },
164                                ) => {
165                                    if let Some(v) = vs.get(*index) {
166                                        response_body = v.to_owned();
167                                    } else {
168                                        // if we can not find the element serde_path_to_error references fall back to just returning the error
169                                        return Err(e.into());
170                                    }
171                                }
172                                (
173                                    serde_json::Value::Object(m),
174                                    serde_path_to_error::Segment::Map { key },
175                                ) => {
176                                    if let Some(v) = m.get(key) {
177                                        response_body = v.to_owned();
178                                    } else {
179                                        // if we can not find the element serde_path_to_error references fall back to just returning the error
180                                        return Err(e.into());
181                                    }
182                                }
183                                _ => {
184                                    // if we can not find the element serde_path_to_error references fall back to just returning the error
185                                    return Err(e.into());
186                                }
187                            }
188                        }
189                        tracing::error!("Value in location path references is: {}", response_body);
190                    }
191                    Err(e.into())
192                }
193            }
194        }
195    }
196}