365 lines
11 KiB
Rust
365 lines
11 KiB
Rust
use std::fmt::Display;
|
|
use std::marker::PhantomData;
|
|
use std::net::{IpAddr, SocketAddr};
|
|
use std::sync::Arc;
|
|
|
|
use log::trace;
|
|
use reqwest::{Certificate, Request};
|
|
use reqwest::header::{HeaderName, HeaderValue};
|
|
use serde::de::DeserializeOwned;
|
|
use tokio::sync::Semaphore;
|
|
|
|
use crate::api::{DomainCollection, DomainExtention, DomainObject, DomainType};
|
|
use crate::{Error, Result};
|
|
|
|
|
|
pub struct Client(Arc<InnerClient>);
|
|
struct InnerClient {
|
|
http: reqwest::Client,
|
|
url: String,
|
|
semaphore: Semaphore
|
|
}
|
|
|
|
#[derive(Default, PartialEq, Eq)]
|
|
enum SslStrategy {
|
|
NoSll,
|
|
IngoreHostname,
|
|
IgnoreCertificate,
|
|
#[default] Strict
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct RequiresLocation;
|
|
#[derive(Default)]
|
|
pub struct RequiresCredentials;
|
|
#[derive(Default)]
|
|
pub struct RequiresNothing;
|
|
#[derive(Default)]
|
|
pub struct ClientBuilder<T> {
|
|
hostname: String,
|
|
sitename: String,
|
|
username: String,
|
|
password: String,
|
|
ssl: SslStrategy,
|
|
root_certificate: Option<Certificate>,
|
|
resolve: Option<SocketAddr>,
|
|
port: u16,
|
|
|
|
_marker: PhantomData<T>
|
|
}
|
|
|
|
impl <T> ClientBuilder<T> {
|
|
fn new() -> ClientBuilder<RequiresLocation> {
|
|
Default::default()
|
|
}
|
|
}
|
|
|
|
impl ClientBuilder<RequiresLocation> {
|
|
pub fn on_host_and_site(self, hostname: String, sitename: String) -> ClientBuilder<RequiresCredentials> {
|
|
ClientBuilder::<RequiresCredentials> {
|
|
hostname, sitename,
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ClientBuilder<RequiresCredentials> {
|
|
pub fn with_credentials(self, username: String, password: String) -> ClientBuilder<RequiresNothing> {
|
|
ClientBuilder {
|
|
hostname: self.hostname,
|
|
sitename: self.sitename,
|
|
username, password,
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
type ClientBuildResult<T> = std::result::Result<T, ClientBuildError>;
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum ClientBuildError {
|
|
#[error("failed to build client: {0}")]
|
|
BuildClient(#[from] reqwest::Error),
|
|
#[error("invalid credentials. thise can only consist of visibly ASCII characters (ie. (32-127))")]
|
|
InvalidCredentials(#[from] reqwest::header::InvalidHeaderValue)
|
|
}
|
|
|
|
impl ClientBuilder<RequiresNothing> {
|
|
pub fn with_specific_port(mut self, port: u16) -> Self {
|
|
self.port = port;
|
|
self
|
|
}
|
|
pub fn without_ssl(mut self) -> Self {
|
|
self.ssl = SslStrategy::NoSll;
|
|
self
|
|
}
|
|
pub fn ignore_hostname_verification(mut self) -> Self {
|
|
self.ssl = SslStrategy::IngoreHostname;
|
|
self
|
|
}
|
|
pub fn ignore_certificate_veritifcation(mut self) -> Self {
|
|
self.ssl = SslStrategy::IgnoreCertificate;
|
|
self
|
|
}
|
|
pub fn with_root_certificate(mut self, certificate: Certificate) -> Self {
|
|
let _ = self.root_certificate.insert(certificate);
|
|
self
|
|
}
|
|
pub fn resolve(mut self, ipaddr: IpAddr) -> Self {
|
|
let _ = self.resolve.insert(SocketAddr::new(ipaddr, 0));
|
|
self
|
|
}
|
|
|
|
fn protocol(&self) -> &str {
|
|
if self.ssl == SslStrategy::NoSll {
|
|
"http"
|
|
} else {
|
|
"https"
|
|
}
|
|
}
|
|
fn basic_auth_header(&self) -> ClientBuildResult<(HeaderName, HeaderValue)> {
|
|
let header = format!("Bearer {} {}", self.username, self.password);
|
|
let mut header = HeaderValue::from_str(&header)?;
|
|
header.set_sensitive(true);
|
|
Ok((reqwest::header::AUTHORIZATION, header))
|
|
}
|
|
pub fn build(self) -> Result<Client> {
|
|
const API_BASE: &str = "check_mk/api/1.0";
|
|
let url = format!(
|
|
"{}://{}:{}/{}/{API_BASE}",
|
|
self.protocol(),
|
|
self.hostname,
|
|
self.port,
|
|
self.sitename,
|
|
);
|
|
trace!("checkmk url: {url}");
|
|
|
|
let http = {
|
|
let headers = [self.basic_auth_header()?].into_iter().collect();
|
|
let mut builder = reqwest::ClientBuilder::new()
|
|
.default_headers(headers)
|
|
.danger_accept_invalid_certs(self.ssl == SslStrategy::IgnoreCertificate)
|
|
.danger_accept_invalid_hostnames(self.ssl == SslStrategy::IngoreHostname);
|
|
|
|
if let Some(sock) = self.resolve {
|
|
builder = builder.resolve(&self.hostname, sock);
|
|
}
|
|
if let Some(certificate) = self.root_certificate {
|
|
builder = builder.add_root_certificate(certificate);
|
|
}
|
|
|
|
builder.build()
|
|
.map_err(ClientBuildError::BuildClient)
|
|
.map_err(Error::BuildClient)?
|
|
};
|
|
|
|
// checkmk api struggles with more than 10 requests at a time
|
|
// not sure if this has been inproved since 2.2
|
|
let semaphore = Semaphore::new(10);
|
|
|
|
Ok(Client(Arc::new(InnerClient { http, url, semaphore })))
|
|
}
|
|
}
|
|
|
|
impl Client {
|
|
pub fn from_parts(url: String, client: reqwest::Client) -> Self {
|
|
Self(Arc::new(InnerClient {
|
|
http: client,
|
|
semaphore: Semaphore::new(10),
|
|
url
|
|
}))
|
|
}
|
|
pub fn builder() -> ClientBuilder<RequiresLocation> {
|
|
Default::default()
|
|
}
|
|
}
|
|
|
|
impl InnerClient {
|
|
pub(crate) fn object_url(&self, domain_type: DomainType, id: impl Display) -> String {
|
|
format!("{}/objects/{}/{}", self.url, domain_type, id)
|
|
}
|
|
pub(crate) fn collection_url(&self, domain_type: DomainType) -> String {
|
|
format!("{}/domain-types/{}/collections/all", self.url, domain_type)
|
|
}
|
|
pub(crate) fn bulk_action_url(&self, domain_type: DomainType, action: BulkAction) -> String {
|
|
format!("{}/domain-types/{}/actions/bulk-{}/invoke",self.url, domain_type, action)
|
|
}
|
|
|
|
pub(crate) async fn handle_request(&self, request: Request) -> Result<String> {
|
|
trace!("sending {}-request to {}", request.method(), request.url());
|
|
let response = self.http
|
|
.execute(request)
|
|
.await
|
|
.map_err(Error::SendRequest)?;
|
|
|
|
let status = response.status();
|
|
let body = response.text()
|
|
.await
|
|
.map_err(Error::ReceiveBody)?;
|
|
|
|
if status.is_success() {
|
|
Ok(body)
|
|
} else {
|
|
let cmkerror = serde_json::from_str(&body)
|
|
.map_err(Error::DeserializeResponse)?;
|
|
Err(Error::CheckmkError(cmkerror))
|
|
}
|
|
}
|
|
pub(crate) async fn invoke_api(&self, request: Request) -> Result<()> {
|
|
let _response = self.handle_request(request).await?;
|
|
Ok(())
|
|
}
|
|
pub(crate) async fn query_api<T: DeserializeOwned>(&self, request: Request) -> Result<T> {
|
|
let response = self.handle_request(request).await?;
|
|
serde_json::from_str(&response).map_err(Error::DeserializeResponse)
|
|
}
|
|
|
|
pub(crate) async fn get_etag(&self, object_url: &str) -> Result<HeaderValue> {
|
|
let mut response = self.http
|
|
.head(object_url)
|
|
.send()
|
|
.await
|
|
.map_err(Error::SendRequest)?;
|
|
|
|
if response.status().is_success() {
|
|
response.headers_mut()
|
|
.remove("ETag")
|
|
.ok_or(Error::MissingHeader("ETag"))
|
|
} else {
|
|
let body = response.text()
|
|
.await
|
|
.map_err(Error::ReceiveBody)?;
|
|
let cmkerror = serde_json::from_str(&body)
|
|
.map_err(Error::DeserializeResponse)?;
|
|
Err(Error::CheckmkError(cmkerror))
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn create_domain_object<E: DomainExtention>(
|
|
&self,
|
|
body: &E::CreationRequest,
|
|
query: &E::CreationQuery
|
|
) -> Result<()> {
|
|
let request = self.http
|
|
.post(self.collection_url(E::DOMAIN_TYPE))
|
|
.json(body)
|
|
.query(query)
|
|
.build()
|
|
.unwrap();
|
|
|
|
self.invoke_api(request).await
|
|
}
|
|
pub(crate) async fn read_domain_object<E: DomainExtention>(
|
|
&self,
|
|
id: impl Display,
|
|
query: &E::ReadQuery
|
|
) -> Result<DomainObject<E>> {
|
|
let request = self.http
|
|
.get(self.object_url(E::DOMAIN_TYPE, id))
|
|
.query(query)
|
|
.build()
|
|
.unwrap();
|
|
|
|
self.query_api(request).await
|
|
}
|
|
pub(crate) async fn update_domain_object<E: DomainExtention>(
|
|
&self,
|
|
id: impl Display,
|
|
request: &E::UpdateRequest,
|
|
) -> Result<()> {
|
|
let url = self.object_url(E::DOMAIN_TYPE, id);
|
|
let etag = self.get_etag(&url).await?;
|
|
let request = self.http
|
|
.put(url)
|
|
.json(&request)
|
|
.header(reqwest::header::IF_MATCH, etag)
|
|
.build()
|
|
.unwrap();
|
|
|
|
self.invoke_api(request).await
|
|
}
|
|
pub(crate) async fn delete_domain_object<E: DomainExtention>(
|
|
&self,
|
|
id: impl Display,
|
|
params: &E::DeleteQuery,
|
|
) -> Result<()> {
|
|
let request = self.http
|
|
.delete(self.object_url(E::DOMAIN_TYPE, id))
|
|
.query(¶ms)
|
|
.build()
|
|
.unwrap();
|
|
|
|
self.invoke_api(request).await
|
|
}
|
|
|
|
pub(crate) async fn bulk_create_domain_objects<E: DomainExtention>(
|
|
&self,
|
|
body: &[E::CreationRequest],
|
|
query: &E::CreationQuery
|
|
) -> Result<()> {
|
|
let request = self.http
|
|
.post(self.bulk_action_url(E::DOMAIN_TYPE, BulkAction::Create))
|
|
.json(body)
|
|
.query(query)
|
|
.build()
|
|
.unwrap();
|
|
|
|
self.invoke_api(request).await
|
|
}
|
|
pub(crate) async fn bulk_read_domain_objects<E: DomainExtention>(
|
|
&self,
|
|
query: &E::ReadQuery,
|
|
) -> Result<Vec<DomainObject<E>>> {
|
|
let request = self.http
|
|
.get(self.collection_url(E::DOMAIN_TYPE))
|
|
.query(query)
|
|
.build()
|
|
.unwrap();
|
|
|
|
let response: DomainCollection<E> = self.query_api(request).await?;
|
|
Ok(response.value)
|
|
}
|
|
pub(crate) async fn bulk_update_domain_objects<E: DomainExtention>(
|
|
&self,
|
|
body: &[E::UpdateRequest],
|
|
) -> Result<()> {
|
|
let request = self.http
|
|
.put(self.bulk_action_url(E::DOMAIN_TYPE, BulkAction::Update))
|
|
.json(body)
|
|
.build()
|
|
.unwrap();
|
|
|
|
self.invoke_api(request).await
|
|
}
|
|
pub(crate) async fn bulk_delete_domain_objects<E: DomainExtention>(
|
|
&self,
|
|
ids: &[String],
|
|
) -> Result<()> {
|
|
let request = self.http
|
|
.post(self.bulk_action_url(E::DOMAIN_TYPE, BulkAction::Delete))
|
|
.json(&ids)
|
|
.build()
|
|
.unwrap();
|
|
|
|
self.invoke_api(request).await
|
|
}
|
|
}
|
|
|
|
pub(crate) enum BulkAction {
|
|
Create,
|
|
// Read,
|
|
Update,
|
|
Delete,
|
|
}
|
|
|
|
impl Display for BulkAction {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
BulkAction::Create => write!(f, "create"),
|
|
// BulkAction::Read => write!(f, "read"),
|
|
BulkAction::Update => write!(f, "update"),
|
|
BulkAction::Delete => write!(f, "delete"),
|
|
}
|
|
}
|
|
}
|