From 804faa62731b19b367f014fd026bb312d59a400a Mon Sep 17 00:00:00 2001 From: Vincent Stuyck Date: Sat, 28 Feb 2026 19:22:06 +0100 Subject: [PATCH] add rulesets api --- src/api/mod.rs | 7 +++ src/api/rules.rs | 138 +++++++++++++++++++++++++++++++++++++++++++++++ src/client.rs | 12 ++--- src/main.rs | 39 ++++++++++++++ 4 files changed, 187 insertions(+), 9 deletions(-) create mode 100644 src/api/rules.rs diff --git a/src/api/mod.rs b/src/api/mod.rs index 4e8bddc..2d3a6aa 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2,6 +2,7 @@ pub mod activations; pub mod folders; pub mod groups; pub mod hosts; +pub mod rules; pub mod tags; use std::fmt::{self, Display}; @@ -22,6 +23,9 @@ pub enum DomainType { ServiceGroupConfig, HostTagGroup, ActivationRun, + Ruleset, + Rule, + Dict } impl fmt::Display for DomainType { @@ -35,6 +39,9 @@ impl fmt::Display for DomainType { DomainType::ServiceGroupConfig => write!(f, "service_group_config"), DomainType::HostTagGroup => write!(f, "host_tag_group"), DomainType::ActivationRun => write!(f, "activation_run"), + DomainType::Ruleset => write!(f, "ruleset"), + DomainType::Rule => write!(f, "rule"), + DomainType::Dict => write!(f, "dict"), } } } diff --git a/src/api/rules.rs b/src/api/rules.rs new file mode 100644 index 0000000..3948dc9 --- /dev/null +++ b/src/api/rules.rs @@ -0,0 +1,138 @@ +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; +#[cfg(feature = "schemars")] +use schemars::JsonSchema; + +use crate::api::{DomainExtension, DomainObject, domain_bulk_read, domain_client}; +use crate::{ApiClient, Client, Result}; + + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars", derive(JsonSchema))] +pub struct Ruleset { + pub name: String, + pub title: String, + // this value is not set when searching for a specific ruleset (dont ask me why) + pub help: Option, + // this value should be set, but never is. wrong documentation? + pub folder: Option, + // this value is not set when searching for a specific ruleset (dont ask me why) + pub item_type: Option, + // this value is not set when searching for a specific ruleset (dont ask me why) + pub item_name: Option, + // now sure what this is supposed to be right now. no documentation + // item_enum: Option<()>, + // this value is not set when searching for a specific ruleset (dont ask me why) + pub match_type: Option, + pub number_of_rules: usize +} + +impl DomainExtension for Ruleset { + const DOMAIN_TYPE: super::DomainType = super::DomainType::Ruleset; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars", derive(JsonSchema))] +#[serde(rename_all = "snake_case")] +pub enum MatchType { + All, + First, + Dict, + List, + Varies +} + +domain_client!(Ruleset, ruleset_api); + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars", derive(JsonSchema))] +pub struct RulesetQuery { + name: Option, + fulltext: Option, + folder: Option, + deprecated: bool, + used: bool, +} + +impl RulesetQuery { + pub fn set_name(&mut self, name: String) { + self.name = Some(name); + } + pub fn with_name(mut self, name: String) -> Self { + self.set_name(name); + self + } + + pub fn set_fulltext(&mut self, fulltext: String) { + self.fulltext = Some(fulltext); + } + pub fn with_fulltext(mut self, fulltext: String) -> Self { + self.set_fulltext(fulltext); + self + } + + pub fn set_folder(&mut self, folder: String) { + self.folder = Some(folder); + } + pub fn with_folder(mut self, folder: String) -> Self { + self.set_folder(folder); + self + } + + pub fn set_deprecated(&mut self, deprecated: bool) { + self.deprecated = deprecated; + } + pub fn with_deprecated(mut self, deprecated: bool) -> Self { + self.set_deprecated(deprecated); + self + } + + pub fn set_used(&mut self, used: bool) { + self.used = used; + } + pub fn with_used(mut self, used: bool) -> Self { + self.set_used(used); + self + } +} + + + +impl ApiClient { + pub async fn read(&self, id: impl Display) -> Result { + #[derive(Deserialize, Serialize)] + struct RulesetCompact { + name: String, + folder: Option, + number_of_rules: usize + } + + impl DomainExtension for RulesetCompact { + const DOMAIN_TYPE: super::DomainType = super::DomainType::Ruleset; + } + + impl From> for Ruleset { + fn from(value: DomainObject) -> Self { + Ruleset { + name: value.extensions.name, + folder: value.extensions.folder, + number_of_rules: value.extensions.number_of_rules, + title: value.title.unwrap_or_default(), + help: None, + item_type: None, + item_name: None, + match_type: None, + + } + } + } + + self.inner + .read_domain_object::(id, &()) + .await + .map(|obj| obj.into()) + } +} + +domain_bulk_read!(Ruleset, RulesetQuery); diff --git a/src/client.rs b/src/client.rs index 1d442d9..555072f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -222,15 +222,9 @@ impl InnerClient { pub(crate) async fn handle_request(&self, request: Request) -> Result { let permit = self.semaphore.acquire().await.unwrap(); debug!("sending {}-request to {}", request.method(), request.url()); - trace!( - "with body: {}", - request - .body() - .as_ref() - .and_then(|b| b.as_bytes()) - .map(String::from_utf8_lossy) - .unwrap_or_default() - ); + if let Some(body) = request.body().as_ref().and_then(|b| b.as_bytes()) { + trace!("with body: {}", String::from_utf8_lossy(body)) + } let response = self .http diff --git a/src/main.rs b/src/main.rs index 7fdf741..f1d1a01 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,6 +33,7 @@ async fn main() -> Result<()> { test_host_groups(client.clone()).await?; test_service_groups(client.clone()).await?; test_contact_groups(client.clone()).await?; + test_rulesets(client.clone()).await?; test_activations(client.clone()).await?; info!("tests done; all pass"); @@ -394,6 +395,44 @@ async fn test_activations(client: Client) -> Result<()> { Ok(()) } +async fn test_rulesets(client: Client) -> Result<()> { + use checkmk_api::api::rules::*; + const KNOWN_RULESET: &str = "inv_exports:software_csv"; + info!("testing rulesets"); + + let api = client.ruleset_api(); + + info!("reading single ruleset '{KNOWN_RULESET}'"); + let ruleset = api + .read(KNOWN_RULESET) + .await + .inspect_err(|e| error!("failed to read ruleset: {e}"))?; + info!("ruleset: {ruleset:#?}"); + + info!("bulk reading all rulesets"); + let all = api + .bulk_read(&RulesetQuery::default()) + .await + .inspect_err(|e| error!("failed to bulk read rulesets: {e}"))?; + info!("total rulesets: {}", all.len()); + + info!("bulk reading only used rulesets"); + let used = api + .bulk_read(&RulesetQuery::default().with_used(true)) + .await + .inspect_err(|e| error!("failed to bulk read used rulesets: {e}"))?; + info!("used rulesets: {}", used.len()); + + info!("bulk reading rulesets with fulltext search"); + let search = api + .bulk_read(&RulesetQuery::default().with_fulltext("memory".to_string())) + .await + .inspect_err(|e| error!("failed to bulk read rulesets by fulltext: {e}"))?; + info!("rulesets matching 'memory': {}", search.len()); + + Ok(()) +} + async fn test_contact_groups(client: Client) -> Result<()> { use checkmk_api::api::groups::*; const TESTGROUP: &str = "test-contact-group";