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            client_builder.tls_certs_only([ca_cert])
55        } else {
56            client_builder
57        };
58        let client = client_builder
59            .build()
60            .map_err(crate::error::Error::CouldNotBuildReqwestClientFromSuppliedInformation)?;
61        let url =
62            url::Url::parse(&config.url).map_err(crate::error::Error::CouldNotParseUrlInConfig)?;
63        let username = config.username.clone();
64        let password = config.password.clone();
65        Ok(Icinga2 {
66            client,
67            url,
68            username,
69            password,
70        })
71    }
72
73    /// create a new Icinga2 instance from a TOML config file
74    ///
75    /// # Errors
76    /// this fails if the configuration file can not be found or parsed
77    /// or the CA certificate file mentioned in the configuration file
78    /// can not be found or parsed
79    pub fn from_config_file(path: &Path) -> Result<Self, crate::error::Error> {
80        let icinga_instance = Icinga2Instance::from_config_file(path)?;
81        Self::from_instance_config(&icinga_instance)
82    }
83
84    /// common code for the REST API calls
85    ///
86    /// # Errors
87    ///
88    /// this returns an error if encoding, the actual request, or decoding of the response fail
89    pub fn rest<ApiEndpoint, Res>(
90        &self,
91        api_endpoint: ApiEndpoint,
92    ) -> Result<Res, crate::error::Error>
93    where
94        ApiEndpoint: RestApiEndpoint,
95        <ApiEndpoint as RestApiEndpoint>::RequestBody: Clone + Serialize + std::fmt::Debug,
96        Res: DeserializeOwned + std::fmt::Debug + RestApiResponse<ApiEndpoint>,
97    {
98        let method = api_endpoint.method()?;
99        let url = api_endpoint.url(&self.url)?;
100        let request_body: Option<std::borrow::Cow<<ApiEndpoint as RestApiEndpoint>::RequestBody>> =
101            api_endpoint.request_body()?;
102        let actual_method = if method == reqwest::Method::GET && request_body.is_some() {
103            reqwest::Method::POST
104        } else {
105            method.to_owned()
106        };
107        let mut req = self.client.request(actual_method, url.to_owned());
108        if method == reqwest::Method::GET && request_body.is_some() {
109            tracing::trace!("Sending GET request with body as POST via X-HTTP-Method-Override");
110            req = req.header(
111                "X-HTTP-Method-Override",
112                reqwest::header::HeaderValue::from_static("GET"),
113            );
114        }
115        req = req.basic_auth(&self.username, Some(&self.password));
116        if let Some(request_body) = request_body {
117            tracing::trace!("Request body:\n{:#?}", request_body);
118            req = req.json(&request_body);
119        }
120        let result = req.send();
121        if let Err(ref e) = result {
122            tracing::error!(%url, %method, "Icinga2 send error: {:?}", e);
123        }
124        let result = result?;
125        let status = result.status();
126        let response_body = result.bytes()?;
127        match from_utf8(&response_body) {
128            Ok(response_body) => {
129                tracing::trace!("Response body:\n{}", &response_body);
130            }
131            Err(e) => {
132                tracing::trace!(
133                    "Response body that could not be parsed as utf8 because of {}:\n{:?}",
134                    &e,
135                    &response_body
136                );
137            }
138        }
139        if status.is_client_error() {
140            tracing::error!(%url, %method, "Icinga2 status error (client error): {:?}", status);
141        } else if status.is_server_error() {
142            tracing::error!(%url, %method, "Icinga2 status error (server error): {:?}", status);
143        }
144        if response_body.is_empty() {
145            Err(crate::error::Error::EmptyResponseBody(status))
146        } else {
147            let jd = &mut serde_json::Deserializer::from_slice(&response_body);
148            match serde_path_to_error::deserialize(jd) {
149                Ok(response_body) => {
150                    tracing::trace!("Parsed response body:\n{:#?}", response_body);
151                    Ok(response_body)
152                }
153                Err(e) => {
154                    let path = e.path();
155                    tracing::error!("Parsing failed at path {}: {}", path.to_string(), e.inner());
156                    if let Ok(response_body) = serde_json::from_slice(&response_body) {
157                        let mut response_body: serde_json::Value = response_body;
158                        for segment in path {
159                            match (response_body, segment) {
160                                (
161                                    serde_json::Value::Array(vs),
162                                    serde_path_to_error::Segment::Seq { index },
163                                ) => {
164                                    if let Some(v) = vs.get(*index) {
165                                        response_body = v.to_owned();
166                                    } else {
167                                        // if we can not find the element serde_path_to_error references fall back to just returning the error
168                                        return Err(e.into());
169                                    }
170                                }
171                                (
172                                    serde_json::Value::Object(m),
173                                    serde_path_to_error::Segment::Map { key },
174                                ) => {
175                                    if let Some(v) = m.get(key) {
176                                        response_body = v.to_owned();
177                                    } else {
178                                        // if we can not find the element serde_path_to_error references fall back to just returning the error
179                                        return Err(e.into());
180                                    }
181                                }
182                                _ => {
183                                    // if we can not find the element serde_path_to_error references fall back to just returning the error
184                                    return Err(e.into());
185                                }
186                            }
187                        }
188                        tracing::error!("Value in location path references is: {}", response_body);
189                    }
190                    Err(e.into())
191                }
192            }
193        }
194    }
195}