Compare commits

..
9 Commits
15 changed files with 1248 additions and 319 deletions
Generated
+12
View File
@@ -152,6 +152,7 @@ dependencies = [
"simplelog", "simplelog",
"thiserror", "thiserror",
"tokio", "tokio",
"uuid",
] ]
[[package]] [[package]]
@@ -1884,6 +1885,17 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb"
dependencies = [
"js-sys",
"serde_core",
"wasm-bindgen",
]
[[package]] [[package]]
name = "uuid-simd" name = "uuid-simd"
version = "0.8.0" version = "0.8.0"
+1
View File
@@ -19,6 +19,7 @@ serde_json = "1.0.145"
simplelog = "0.12.2" simplelog = "0.12.2"
thiserror = "2.0.17" thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["full"] } tokio = { version = "1.48.0", features = ["full"] }
uuid = { version = "1.21.0", features = ["serde"] }
[features] [features]
schemars = ["dep:schemars"] schemars = ["dep:schemars"]
+163 -19
View File
@@ -1,20 +1,72 @@
use serde::{Deserialize, Serialize}; use std::fmt::Display;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::api::{DomainCollection, DomainExtension, DomainObject, domain_client, domain_read};
use crate::{ApiClient, Client, Result}; use crate::{ApiClient, Client, Result};
domain_client!(ActivationStatus, activation_api);
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))] #[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct ChangeActivation; pub struct ActivationStatus {
sites: Vec<String>,
is_running: bool,
force_foreign_changes: bool,
time_started: DateTime<Utc>,
changes: Vec<PendingChange>,
#[serde(default)]
status_per_site: Vec<SiteStatus>,
}
impl DomainExtension for ActivationStatus {
const DOMAIN_TYPE: super::DomainType = super::DomainType::ActivationRun;
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))] #[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct PendingChange; pub struct PendingChange {
id: Uuid,
user_id: Option<String>,
action_name: String,
text: String,
time: DateTime<Utc>,
}
impl From<&Client> for ApiClient<ChangeActivation> { #[derive(Debug, Clone, Serialize, Deserialize)]
fn from(value: &Client) -> Self { #[cfg_attr(feature = "schemars", derive(JsonSchema))]
Self::new(value) pub struct SiteStatus {
} site: String,
phase: ChangePhase,
state: ChangeState,
status_text: String,
status_details: String,
start_time: DateTime<Utc>,
end_time: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ChangePhase {
Initialized,
Queued,
Started,
Sync,
Activate,
Finishing,
Done,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ChangeState {
Success,
Error,
Warning,
} }
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
@@ -22,23 +74,115 @@ impl From<&Client> for ApiClient<ChangeActivation> {
pub struct ActivateChangeRequest { pub struct ActivateChangeRequest {
redirect: bool, redirect: bool,
sites: Vec<String>, sites: Vec<String>,
force_foreign_changes: bool force_foreign_changes: bool,
} }
impl ApiClient<ChangeActivation> { #[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub async fn activate(&self) -> Result<()> { #[cfg_attr(feature = "schemars", derive(JsonSchema))]
todo!() pub struct ActivationOptions {
redirect: bool,
sites: Vec<String>,
force_foreign_changes: bool,
}
impl ActivationOptions {
pub fn set_redirect(&mut self, redirect: bool) {
self.redirect = redirect
} }
pub async fn await_activation(&self, activation_id: &str) -> Result<()> { pub fn with_redirect(mut self, redirect: bool) -> Self {
todo!() self.set_redirect(redirect);
self
} }
pub async fn read_current_activations(&self) -> Result<Vec<ChangeActivation>> { pub fn set_force_foreign_changes(&mut self, force: bool) {
todo!() self.force_foreign_changes = force;
} }
pub async fn bulk_read_pending_changes(&self) -> Result<Vec<PendingChange>> { pub fn with_force_foreign_changes(mut self, force: bool) -> Self {
todo!() self.set_force_foreign_changes(force);
self
} }
pub async fn read_activation_status(&self, activation_id: &str) -> Result<ChangeActivation> {
todo!() pub fn set_sites(&mut self, sites: Vec<String>) {
self.sites = sites;
}
pub fn with_sites(mut self, sites: Vec<String>) -> Self {
self.set_sites(sites);
self
}
pub fn set_site(&mut self, site: String) {
self.sites.push(site);
}
pub fn with_site(mut self, site: String) -> Self {
self.set_site(site);
self
}
}
domain_read!(ActivationStatus);
impl ApiClient<ActivationStatus> {
pub async fn activate_pending_changes(&self, options: &ActivationOptions) -> Result<String> {
let etag = {
let url = format!("{}/pending_changes", self.collection_root());
self.inner.get_etag(&url).await
}?;
let url = format!(
"{}/domain-types/{}/actions/activate-changes/invoke",
self.inner.url,
ActivationStatus::DOMAIN_TYPE
);
let request = self
.inner
.http
.post(url)
.json(&options)
.header(reqwest::header::IF_MATCH, etag)
.build()
.unwrap();
let response: DomainObject<ActivationStatus> = self.inner.query_api(request).await?;
Ok(response.id)
}
pub async fn await_completion(&self, activation_id: impl Display) -> Result<()> {
let url = format!(
"{}/objects/{}/{}/actions/wait-for-completion/invoke",
self.inner.url,
ActivationStatus::DOMAIN_TYPE,
activation_id
);
let request = self.inner.http.get(url).build().unwrap();
self.inner.invoke_api(request).await
}
#[inline]
fn collection_root(&self) -> String {
format!(
"{}/domain-types/{}/collections",
self.inner.url,
ActivationStatus::DOMAIN_TYPE
)
}
pub async fn read_running_activations(&self) -> Result<Vec<ActivationStatus>> {
let url = format!("{}/running", self.collection_root());
let request = self.inner.http.get(url).build().unwrap();
let collection: DomainCollection<_> = self.inner.query_api(request).await?;
Ok(collection.into())
}
pub async fn read_pending_changes(&self) -> Result<Vec<PendingChange>> {
let url = format!("{}/pending_changes", self.collection_root());
let request = self.inner.http.get(url).build().unwrap();
#[derive(Deserialize)]
struct ChangeCollection {
value: Vec<PendingChange>,
}
let collection: ChangeCollection = self.inner.query_api(request).await?;
Ok(collection.value)
} }
} }
+89 -6
View File
@@ -8,9 +8,10 @@ use serde_json::Value;
#[cfg(feature = "schemars")] #[cfg(feature = "schemars")]
use schemars::JsonSchema; use schemars::JsonSchema;
use crate::api::rules::contactgroups::HostContactGroups; use crate::api::{
use crate::api::rules::snmp::SnmpCommunity; DomainExtension, domain_bulk_read, domain_bulk_update, domain_client, domain_create,
use crate::api::{DomainExtension, domain_bulk_read, domain_bulk_update, domain_client, domain_create, domain_delete, domain_read, domain_update}; domain_delete, domain_read, domain_update,
};
use crate::{ApiClient, Client, Result}; use crate::{ApiClient, Client, Result};
pub const ROOT_FOLDER: &str = "/"; pub const ROOT_FOLDER: &str = "/";
@@ -24,6 +25,7 @@ pub struct FolderConfig {
pub attributes: FolderAttributes, pub attributes: FolderAttributes,
} }
#[allow(dead_code)]
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))] #[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct FolderAttributes { pub struct FolderAttributes {
@@ -46,7 +48,6 @@ pub struct FolderAttributes {
#[serde(default, flatten, skip_serializing_if = "HashMap::is_empty")] #[serde(default, flatten, skip_serializing_if = "HashMap::is_empty")]
pub tags: HashMap<String, String>, pub tags: HashMap<String, String>,
// these fields are used to prevent tags from taking them up // these fields are used to prevent tags from taking them up
#[serde(default)] #[serde(default)]
management_snmp_community: Option<SnmpCommunity>, management_snmp_community: Option<SnmpCommunity>,
@@ -112,8 +113,7 @@ impl FolderAttributes {
} }
pub fn add_tag(&mut self, taggroup: impl Display, tagvalue: String) { pub fn add_tag(&mut self, taggroup: impl Display, tagvalue: String) {
self.tags self.tags.insert(format!("tag_{taggroup}"), tagvalue);
.insert(format!("tag_{taggroup}"), tagvalue);
} }
pub fn with_tag(mut self, taggroup: impl Display, tagvalue: String) -> Self { pub fn with_tag(mut self, taggroup: impl Display, tagvalue: String) -> Self {
self.add_tag(taggroup, tagvalue); self.add_tag(taggroup, tagvalue);
@@ -130,6 +130,89 @@ pub struct MetaData {
pub created_by: Option<String>, pub created_by: Option<String>,
} }
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub enum SnmpV3AuthProtocol {
#[serde(rename = "MD5-96")]
Md5,
#[serde(rename = "SHA-1-96")]
Sha96,
#[serde(rename = "SHA-2-224")]
Sha224,
#[serde(rename = "SHA-2-256")]
Sha256,
#[serde(rename = "SHA-2-384")]
Sha384,
#[serde(rename = "SHA-2-512")]
Sha512,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct SnmpV3Auth {
pub auth_protocol: SnmpV3AuthProtocol,
pub security_name: String,
pub auth_password: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub enum SnmpV3PrivProtocol {
#[serde(rename = "CBC-DES")]
Des,
#[serde(rename = "3DES-EDE")]
Des3,
#[serde(rename = "AES-128")]
Aes128,
#[serde(rename = "AES-192")]
Aes192,
#[serde(rename = "AES-256")]
Aes256,
#[serde(rename = "AES-192-Blumenthal")]
Aes192Blumenthal,
#[serde(rename = "AES-256-Blumenthal")]
Aes256Blumenthal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct SnmpV3Priv {
pub privacy_protocol: SnmpV3PrivProtocol,
pub privacy_password: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(tag = "type")]
pub enum SnmpCommunity {
V1V2Community {
community: String,
},
NoAuthNoPriv {
security_name: String,
},
AuthNoPriv {
#[serde(flatten)]
auth: SnmpV3Auth,
},
AuthPriv {
#[serde(flatten)]
authentication: SnmpV3Auth,
#[serde(flatten)]
privacy: SnmpV3Priv,
},
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct HostContactGroups {
pub groups: Vec<String>,
pub r#use: bool,
pub use_for_services: bool,
pub recurse_use: bool,
pub recurse_perms: bool,
}
impl DomainExtension for FolderConfig { impl DomainExtension for FolderConfig {
const DOMAIN_TYPE: super::DomainType = super::DomainType::FolderConfig; const DOMAIN_TYPE: super::DomainType = super::DomainType::FolderConfig;
} }
+28 -47
View File
@@ -1,33 +1,17 @@
use core::ops::Deref; use core::ops::Deref;
use serde::{Deserialize, Serialize};
#[cfg(feature = "schemars")] #[cfg(feature = "schemars")]
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::api::{ use crate::api::{DomainCollection, DomainObject, DomainType, domain_client};
DomainObject, DomainType
};
use crate::{ApiClient, Client, Result}; use crate::{ApiClient, Client, Result};
impl Client {
pub fn host_group_api(&self) -> ApiClient<HostGroup> {
self.into()
}
pub fn service_group_api(&self) -> ApiClient<ServiceGroup> {
self.into()
}
pub fn contact_group_api(&self) -> ApiClient<ContactGroup> {
self.into()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))] #[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct Group { pub struct Group {
pub name: String, pub name: String,
pub alias: String pub alias: String,
} }
impl Group { impl Group {
@@ -42,12 +26,12 @@ impl From<DomainObject<serde_json::Value>> for Group {
} }
} }
macro_rules! domain_group { macro_rules! domain_group {
($id:ident, $typ:expr) => { ($id:ident, $typ:expr, $func:ident) => {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))] #[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct $id(Group); pub struct $id(Group);
domain_client!($id, $func);
impl Deref for $id { impl Deref for $id {
type Target = Group; type Target = Group;
@@ -69,25 +53,18 @@ macro_rules! domain_group {
} }
} }
impl From<&Client> for ApiClient<$id> {
fn from(value: &Client) -> Self {
Self::new(value)
}
}
impl ApiClient<$id> { impl ApiClient<$id> {
pub async fn create(&self, group: &$id) -> Result<()> { pub async fn create(&self, group: &$id) -> Result<()> {
self.inner.create($typ, group, &()).await self.inner.create($typ, group, &()).await
} }
pub async fn read(&self, id: &str) -> Result<$id> { pub async fn read(&self, id: &str) -> Result<$id> {
let obj: DomainObject<serde_json::Value> = let obj: DomainObject<serde_json::Value> = self.inner.read($typ, id, &()).await?;
self.inner.read($typ, id, &()).await?;
Ok($id::from(obj)) Ok($id::from(obj))
} }
pub async fn update(&self, id: &str, alias: &str) -> Result<()> { pub async fn update(&self, id: &str, alias: &str) -> Result<()> {
#[derive(Serialize)] #[derive(Serialize)]
struct Body<'a> { struct Body<'a> {
alias: &'a str alias: &'a str,
} }
self.inner.update($typ, id, &Body { alias }).await self.inner.update($typ, id, &Body { alias }).await
@@ -100,46 +77,50 @@ macro_rules! domain_group {
self.inner.bulk_create($typ, entries, &()).await self.inner.bulk_create($typ, entries, &()).await
} }
pub async fn bulk_read(&self) -> Result<Vec<$id>> { pub async fn bulk_read(&self) -> Result<Vec<$id>> {
let entries: Vec<DomainObject<serde_json::Value>> = let collection: DomainCollection<serde_json::Value> =
self.inner.bulk_read($typ, &()).await?; self.inner.bulk_read($typ, &()).await?;
let objects = entries.into_iter() let groups = collection.value.into_iter().map($id::from).collect();
.map($id::from) Ok(groups)
.collect();
Ok(objects)
} }
pub async fn bulk_update(&self, entries: &[Group]) -> Result<()> { pub async fn bulk_update(&self, entries: &[Group]) -> Result<()> {
#[derive(Serialize)] #[derive(Serialize)]
struct UpdateAttributes<'a> { struct UpdateAttributes<'a> {
alias: &'a str alias: &'a str,
} }
#[derive(Serialize)] #[derive(Serialize)]
struct UpdateRequest<'a> { struct UpdateRequest<'a> {
name: &'a str, name: &'a str,
attributes: UpdateAttributes<'a> attributes: UpdateAttributes<'a>,
} }
impl <'a> UpdateRequest<'a> { impl<'a> UpdateRequest<'a> {
fn from(value: &'a Group) -> Self { fn from(value: &'a Group) -> Self {
Self { Self {
name: value.name.as_str(), name: value.name.as_str(),
attributes: UpdateAttributes { attributes: UpdateAttributes {
alias: value.alias.as_str() alias: value.alias.as_str(),
} },
} }
} }
} }
let entries = entries.iter() let entries = entries.iter().map(UpdateRequest::from).collect::<Vec<_>>();
.map(UpdateRequest::from)
.collect::<Vec<_>>();
self.inner.bulk_update($typ, &entries).await self.inner.bulk_update($typ, &entries).await
} }
pub async fn bulk_delete(&self, entries: &[String]) -> Result<()> { pub async fn bulk_delete(&self, entries: &[String]) -> Result<()> {
self.inner.bulk_delete($typ, entries).await self.inner.bulk_delete($typ, entries).await
} }
} }
} };
} }
domain_group!(HostGroup, DomainType::HostGroupConfig); domain_group!(HostGroup, DomainType::HostGroupConfig, host_group_api);
domain_group!(ServiceGroup, DomainType::ServiceGroupConfig); domain_group!(
domain_group!(ContactGroup, DomainType::ContactGroupConfig); ServiceGroup,
DomainType::ServiceGroupConfig,
service_group_api
);
domain_group!(
ContactGroup,
DomainType::ContactGroupConfig,
contact_group_api
);
+4 -5
View File
@@ -1,17 +1,17 @@
use std::fmt::Display; use std::fmt::Display;
use std::net::{Ipv4Addr, Ipv6Addr}; use std::net::{Ipv4Addr, Ipv6Addr};
use serde::{Deserialize, Serialize};
#[cfg(feature = "schemars")] #[cfg(feature = "schemars")]
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::api::folders::FolderAttributes; use crate::api::folders::FolderAttributes;
use crate::api::{ use crate::api::{
DomainExtension, domain_bulk_create, domain_bulk_delete, domain_bulk_read, domain_bulk_update, domain_client, domain_create, domain_delete, domain_read, domain_update DomainExtension, domain_bulk_create, domain_bulk_delete, domain_bulk_read, domain_bulk_update,
domain_client, domain_create, domain_delete, domain_read, domain_update,
}; };
use crate::{ApiClient, Client, Result}; use crate::{ApiClient, Client, Result};
domain_client!(HostConfig, host_api); domain_client!(HostConfig, host_api);
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
@@ -25,7 +25,6 @@ pub struct HostConfig {
pub cluster_nodes: Option<Vec<String>>, pub cluster_nodes: Option<Vec<String>>,
} }
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))] #[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct HostAttributes { pub struct HostAttributes {
@@ -41,7 +40,7 @@ pub struct HostAttributes {
additional_ipv6addresses: Vec<Ipv6Addr>, additional_ipv6addresses: Vec<Ipv6Addr>,
#[serde(flatten, default)] #[serde(flatten, default)]
folder_attributes: FolderAttributes folder_attributes: FolderAttributes,
} }
impl DomainExtension for HostConfig { impl DomainExtension for HostConfig {
+59 -50
View File
@@ -1,8 +1,8 @@
pub mod activations; pub mod activations;
pub mod folders; pub mod folders;
pub mod hosts;
pub mod groups; pub mod groups;
pub(crate) mod rules; pub mod hosts;
pub mod rules;
pub mod tags; pub mod tags;
use std::fmt::{self, Display}; use std::fmt::{self, Display};
@@ -21,7 +21,11 @@ pub enum DomainType {
ContactGroupConfig, ContactGroupConfig,
HostGroupConfig, HostGroupConfig,
ServiceGroupConfig, ServiceGroupConfig,
HostTagGroup HostTagGroup,
ActivationRun,
Ruleset,
Rule,
Dict,
} }
impl fmt::Display for DomainType { impl fmt::Display for DomainType {
@@ -34,6 +38,10 @@ impl fmt::Display for DomainType {
DomainType::HostGroupConfig => write!(f, "host_group_config"), DomainType::HostGroupConfig => write!(f, "host_group_config"),
DomainType::ServiceGroupConfig => write!(f, "service_group_config"), DomainType::ServiceGroupConfig => write!(f, "service_group_config"),
DomainType::HostTagGroup => write!(f, "host_tag_group"), 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"),
} }
} }
} }
@@ -89,20 +97,22 @@ pub(crate) struct DomainCollection<E> {
// pub extensions: Option<E>, // pub extensions: Option<E>,
} }
impl<E> From<DomainCollection<E>> for Vec<E> {
fn from(value: DomainCollection<E>) -> Self {
value.value.into_iter().map(|obj| obj.extensions).collect()
}
}
pub trait DomainExtension: DeserializeOwned + Serialize { pub trait DomainExtension: DeserializeOwned + Serialize {
const DOMAIN_TYPE: DomainType; const DOMAIN_TYPE: DomainType;
} }
impl InnerClient { impl InnerClient {
pub(crate) async fn create_domain_object<E, O, Q>( pub(crate) async fn create_domain_object<E, O, Q>(&self, object: &O, query: &Q) -> Result<()>
&self,
object: &O,
query: &Q,
) -> Result<()>
where where
E: DomainExtension, E: DomainExtension,
O: Serialize, O: Serialize,
Q: Serialize Q: Serialize,
{ {
self.create(E::DOMAIN_TYPE, object, query).await self.create(E::DOMAIN_TYPE, object, query).await
} }
@@ -113,7 +123,7 @@ impl InnerClient {
) -> Result<DomainObject<E>> ) -> Result<DomainObject<E>>
where where
E: DomainExtension, E: DomainExtension,
Q: Serialize Q: Serialize,
{ {
self.read(E::DOMAIN_TYPE, id, query).await self.read(E::DOMAIN_TYPE, id, query).await
} }
@@ -124,18 +134,14 @@ impl InnerClient {
) -> Result<()> ) -> Result<()>
where where
E: DomainExtension, E: DomainExtension,
O: Serialize O: Serialize,
{ {
self.update(E::DOMAIN_TYPE, id, object).await self.update(E::DOMAIN_TYPE, id, object).await
} }
pub(crate) async fn delete_domain_object<E, Q>( pub(crate) async fn delete_domain_object<E, Q>(&self, id: impl Display, query: &Q) -> Result<()>
&self,
id: impl Display,
query: &Q,
) -> Result<()>
where where
E: DomainExtension, E: DomainExtension,
Q: Serialize Q: Serialize,
{ {
self.delete(E::DOMAIN_TYPE, id, query).await self.delete(E::DOMAIN_TYPE, id, query).await
} }
@@ -148,43 +154,35 @@ impl InnerClient {
where where
E: DomainExtension, E: DomainExtension,
O: Serialize, O: Serialize,
Q: Serialize Q: Serialize,
{ {
self.bulk_create(E::DOMAIN_TYPE, entries, query).await self.bulk_create(E::DOMAIN_TYPE, entries, query).await
} }
pub(crate) async fn bulk_read_domain_objects<E, Q>( pub(crate) async fn bulk_read_domain_objects<E, Q>(
&self, &self,
query: &Q, query: &Q,
) -> Result<Vec<DomainObject<E>>> ) -> Result<DomainCollection<E>>
where where
E: DomainExtension, E: DomainExtension,
Q: Serialize Q: Serialize,
{ {
self.bulk_read(E::DOMAIN_TYPE, query).await self.bulk_read(E::DOMAIN_TYPE, query).await
} }
pub(crate) async fn bulk_update_domain_objects<E, O>( pub(crate) async fn bulk_update_domain_objects<E, O>(&self, entries: &[O]) -> Result<()>
&self,
entries: &[O],
) -> Result<()>
where where
E: DomainExtension, E: DomainExtension,
O: Serialize O: Serialize,
{ {
self.bulk_update(E::DOMAIN_TYPE, entries).await self.bulk_update(E::DOMAIN_TYPE, entries).await
} }
pub(crate) async fn bulk_delete_domain_objects<E>( pub(crate) async fn bulk_delete_domain_objects<E>(&self, entries: &[String]) -> Result<()>
&self,
entries: &[String],
) -> Result<()>
where where
E: DomainExtension E: DomainExtension,
{ {
self.bulk_delete(E::DOMAIN_TYPE, entries).await self.bulk_delete(E::DOMAIN_TYPE, entries).await
} }
} }
macro_rules! domain_client { macro_rules! domain_client {
($id:ident, $func:ident) => { ($id:ident, $func:ident) => {
impl Client { impl Client {
@@ -200,7 +198,7 @@ macro_rules! domain_client {
Self::new(value) Self::new(value)
} }
} }
} };
} }
pub(super) use domain_client; pub(super) use domain_client;
@@ -208,14 +206,18 @@ macro_rules! domain_create {
($id:ident, $object:ident) => { ($id:ident, $object:ident) => {
impl ApiClient<$id> { impl ApiClient<$id> {
pub async fn create(&self, object: &$object) -> Result<()> { pub async fn create(&self, object: &$object) -> Result<()> {
self.inner.create_domain_object::<$id, _, _>(object, &()).await self.inner
.create_domain_object::<$id, _, _>(object, &())
.await
} }
} }
}; };
($id:ident, $object:ident, $qry:ident) => { ($id:ident, $object:ident, $qry:ident) => {
impl ApiClient<$id> { impl ApiClient<$id> {
pub async fn create(&self, object: &$object, query: &$qry) -> Result<()> { pub async fn create(&self, object: &$object, query: &$qry) -> Result<()> {
self.inner.create_domain_object::<$id, _, _>(object, query).await self.inner
.create_domain_object::<$id, _, _>(object, query)
.await
} }
} }
}; };
@@ -226,7 +228,9 @@ macro_rules! domain_read {
($id:ident) => { ($id:ident) => {
impl ApiClient<$id> { impl ApiClient<$id> {
pub async fn read(&self, id: impl Display) -> Result<$id> { pub async fn read(&self, id: impl Display) -> Result<$id> {
self.inner.read_domain_object::<$id, _>(id, &()).await self.inner
.read_domain_object::<$id, _>(id, &())
.await
.map(|obj| obj.extensions) .map(|obj| obj.extensions)
} }
} }
@@ -234,7 +238,9 @@ macro_rules! domain_read {
($id:ident, $qry:ident) => { ($id:ident, $qry:ident) => {
impl ApiClient<$id> { impl ApiClient<$id> {
pub async fn read(&self, id: impl Display, query: &$qry) -> Result<$id> { pub async fn read(&self, id: impl Display, query: &$qry) -> Result<$id> {
self.inner.read_domain_object::<$id, _>(id, &query).await self.inner
.read_domain_object::<$id, _>(id, &query)
.await
.map(|obj| obj.extensions) .map(|obj| obj.extensions)
} }
} }
@@ -275,14 +281,18 @@ macro_rules! domain_bulk_create {
($id:ident, $object:ident) => { ($id:ident, $object:ident) => {
impl ApiClient<$id> { impl ApiClient<$id> {
pub async fn bulk_create(&self, entries: &[$object]) -> Result<()> { pub async fn bulk_create(&self, entries: &[$object]) -> Result<()> {
self.inner.bulk_create_domain_objects::<$id, _, _>(entries, &()).await self.inner
.bulk_create_domain_objects::<$id, _, _>(entries, &())
.await
} }
} }
}; };
($id:ident, $object:ident, $qry:ident) => { ($id:ident, $object:ident, $qry:ident) => {
impl ApiClient<$id> { impl ApiClient<$id> {
pub async fn bulk_create(&self, entries: &[$object], query: &$qry) -> Result<()> { pub async fn bulk_create(&self, entries: &[$object], query: &$qry) -> Result<()> {
self.inner.bulk_create_domain_objects::<$id, _, _>(entries, query).await self.inner
.bulk_create_domain_objects::<$id, _, _>(entries, query)
.await
} }
} }
}; };
@@ -293,22 +303,19 @@ macro_rules! domain_bulk_read {
($id:ident) => { ($id:ident) => {
impl ApiClient<$id> { impl ApiClient<$id> {
pub async fn bulk_read(&self) -> Result<Vec<$id>> { pub async fn bulk_read(&self) -> Result<Vec<$id>> {
let objs = self.inner.bulk_read_domain_objects::<$id, _>(&()).await?; let collection = self.inner.bulk_read_domain_objects::<$id, _>(&()).await?;
let objs = objs.into_iter() Ok(collection.into())
.map(|obj| obj.extensions)
.collect();
Ok(objs)
} }
} }
}; };
($id:ident, $qry:ident) => { ($id:ident, $qry:ident) => {
impl ApiClient<$id> { impl ApiClient<$id> {
pub async fn bulk_read(&self, query: &$qry) -> Result<Vec<$id>> { pub async fn bulk_read(&self, query: &$qry) -> Result<Vec<$id>> {
let objs = self.inner.bulk_read_domain_objects::<$id, _>(&query).await?; let collection = self
let objs = objs.into_iter() .inner
.map(|obj| obj.extensions) .bulk_read_domain_objects::<$id, _>(&query)
.collect(); .await?;
Ok(objs) Ok(collection.into())
} }
} }
}; };
@@ -319,7 +326,9 @@ macro_rules! domain_bulk_update {
($id:ident, $object:ident) => { ($id:ident, $object:ident) => {
impl ApiClient<$id> { impl ApiClient<$id> {
pub async fn bulk_update(&self, entries: &[$object]) -> Result<()> { pub async fn bulk_update(&self, entries: &[$object]) -> Result<()> {
self.inner.bulk_update_domain_objects::<$id, _>(entries).await self.inner
.bulk_update_domain_objects::<$id, _>(entries)
.await
} }
} }
}; };
+695
View File
@@ -0,0 +1,695 @@
use std::fmt::Display;
#[cfg(feature = "schemars")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::api::{
DomainExtension, DomainObject, domain_bulk_read, domain_client, domain_create, domain_delete,
domain_update,
};
use crate::{ApiClient, Client, Error, 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<String>,
// this value should be set, but never is. wrong documentation?
pub folder: Option<String>,
// this value is not set when searching for a specific ruleset (dont ask me why)
pub item_type: Option<String>,
// this value is not set when searching for a specific ruleset (dont ask me why)
pub item_name: Option<String>,
// 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<MatchType>,
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<String>,
fulltext: Option<String>,
folder: Option<String>,
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<Ruleset> {
pub async fn read(&self, id: impl Display) -> Result<Ruleset> {
#[derive(Deserialize, Serialize)]
struct RulesetCompact {
name: String,
folder: Option<String>,
number_of_rules: usize,
}
impl DomainExtension for RulesetCompact {
const DOMAIN_TYPE: super::DomainType = super::DomainType::Ruleset;
}
impl From<DomainObject<RulesetCompact>> for Ruleset {
fn from(value: DomainObject<RulesetCompact>) -> 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::<RulesetCompact, _>(id, &())
.await
.map(|obj| obj.into())
}
}
domain_bulk_read!(Ruleset, RulesetQuery);
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct Rule {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub folder_index: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<Uuid>,
pub ruleset: String,
pub folder: String,
pub properties: RuleProperties,
pub value_raw: String,
pub conditions: RuleCondition,
}
impl DomainExtension for Rule {
const DOMAIN_TYPE: super::DomainType = super::DomainType::Rule;
}
impl Rule {
pub fn new(ruleset: String, folder: String) -> Self {
Self {
ruleset,
folder,
..Default::default()
}
}
pub fn set_ruleset(&mut self, ruleset: String) {
self.ruleset = ruleset;
}
pub fn with_ruleset(mut self, ruleset: String) -> Self {
self.set_ruleset(ruleset);
self
}
pub fn set_folder(&mut self, folder: String) {
self.folder = folder;
}
pub fn with_folder(mut self, folder: String) -> Self {
self.set_folder(folder);
self
}
pub fn set_properties(&mut self, properties: RuleProperties) {
self.properties = properties;
}
pub fn with_properties(mut self, properties: RuleProperties) -> Self {
self.set_properties(properties);
self
}
pub fn set_value_raw(&mut self, value_raw: String) {
self.value_raw = value_raw;
}
pub fn with_value_raw(mut self, value_raw: String) -> Self {
self.set_value_raw(value_raw);
self
}
pub fn set_conditions(&mut self, conditions: RuleCondition) {
self.conditions = conditions;
}
pub fn with_conditions(mut self, conditions: RuleCondition) -> Self {
self.set_conditions(conditions);
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct RuleProperties {
pub description: String,
pub comment: String,
pub documentation_url: String,
pub disabled: bool,
}
impl RuleProperties {
pub fn new(description: String) -> Self {
Self {
description,
..Default::default()
}
}
pub fn set_description(&mut self, description: String) {
self.description = description;
}
pub fn with_description(mut self, description: String) -> Self {
self.set_description(description);
self
}
pub fn set_comment(&mut self, comment: String) {
self.comment = comment;
}
pub fn with_comment(mut self, comment: String) -> Self {
self.set_comment(comment);
self
}
pub fn set_documentation_url(&mut self, url: String) {
self.documentation_url = url;
}
pub fn with_documentation_url(mut self, url: String) -> Self {
self.set_documentation_url(url);
self
}
pub fn set_disabled(&mut self, disabled: bool) {
self.disabled = disabled;
}
pub fn with_disabled(mut self, disabled: bool) -> Self {
self.set_disabled(disabled);
self
}
}
// TODO: implement
pub trait RuleMatch {
fn matches(&self, other: Self) -> bool;
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct RuleCondition {
pub host_name: Option<MatchCondition>,
pub host_tags: Vec<HostTagCondition>,
pub host_label_groups: Vec<LabelGroupCondition>,
pub service_label_groups: Vec<LabelGroupCondition>,
pub service_description: Option<MatchCondition>,
}
impl RuleCondition {
/// Match hosts whose name is one of the given values.
pub fn match_hosts(&mut self, hosts: Vec<String>) {
self.host_name = Some(MatchCondition {
match_on: hosts,
operator: MatchOperator::OneOf,
});
}
pub fn matching_hosts(mut self, hosts: Vec<String>) -> Self {
self.match_hosts(hosts);
self
}
/// Exclude hosts whose name is one of the given values.
pub fn exclude_hosts(&mut self, hosts: Vec<String>) {
self.host_name = Some(MatchCondition {
match_on: hosts,
operator: MatchOperator::NoneOf,
});
}
pub fn excluding_hosts(mut self, hosts: Vec<String>) -> Self {
self.exclude_hosts(hosts);
self
}
/// Clear any host name condition.
pub fn clear_hosts(&mut self) {
self.host_name = None;
}
/// Match services whose description is one of the given values.
pub fn match_services(&mut self, services: Vec<String>) {
self.service_description = Some(MatchCondition {
match_on: services,
operator: MatchOperator::OneOf,
});
}
pub fn matching_services(mut self, services: Vec<String>) -> Self {
self.match_services(services);
self
}
/// Exclude services whose description is one of the given values.
pub fn exclude_services(&mut self, services: Vec<String>) {
self.service_description = Some(MatchCondition {
match_on: services,
operator: MatchOperator::NoneOf,
});
}
pub fn excluding_services(mut self, services: Vec<String>) -> Self {
self.exclude_services(services);
self
}
/// Clear any service description condition.
pub fn clear_services(&mut self) {
self.service_description = None;
}
/// Require the host tag `key` to equal `value`.
pub fn require_host_tag(&mut self, key: String, value: String) {
self.host_tags.push(HostTagCondition {
key,
match_on: TagMatchOperator::Is(value),
});
}
pub fn with_host_tag(mut self, key: String, value: String) -> Self {
self.require_host_tag(key, value);
self
}
/// Require the host tag `key` to not equal `value`.
pub fn exclude_host_tag(&mut self, key: String, value: String) {
self.host_tags.push(HostTagCondition {
key,
match_on: TagMatchOperator::IsNot(value),
});
}
pub fn without_host_tag(mut self, key: String, value: String) -> Self {
self.exclude_host_tag(key, value);
self
}
/// Require the host tag `key` to be one of `values`.
pub fn require_any_host_tag(&mut self, key: String, values: Vec<String>) {
self.host_tags.push(HostTagCondition {
key,
match_on: TagMatchOperator::OneOff(values),
});
}
pub fn with_any_host_tag(mut self, key: String, values: Vec<String>) -> Self {
self.require_any_host_tag(key, values);
self
}
/// Require the host tag `key` to be none of `values`.
pub fn exclude_any_host_tag(&mut self, key: String, values: Vec<String>) {
self.host_tags.push(HostTagCondition {
key,
match_on: TagMatchOperator::NoneOff(values),
});
}
pub fn without_any_host_tag(mut self, key: String, values: Vec<String>) -> Self {
self.exclude_any_host_tag(key, values);
self
}
/// Add a host label group that must ALL match (AND).
pub fn require_host_labels(&mut self, labels: Vec<LabelCondition>) {
self.host_label_groups.push(LabelGroupCondition {
operator: LabelOperator::And,
label_group: labels,
});
}
pub fn with_host_labels(mut self, labels: Vec<LabelCondition>) -> Self {
self.require_host_labels(labels);
self
}
/// Add a host label group where ANY must match (OR).
pub fn require_any_host_label(&mut self, labels: Vec<LabelCondition>) {
self.host_label_groups.push(LabelGroupCondition {
operator: LabelOperator::Or,
label_group: labels,
});
}
pub fn with_any_host_label(mut self, labels: Vec<LabelCondition>) -> Self {
self.require_any_host_label(labels);
self
}
/// Add a host label group that must NOT match (NOT).
pub fn exclude_host_labels(&mut self, labels: Vec<LabelCondition>) {
self.host_label_groups.push(LabelGroupCondition {
operator: LabelOperator::Not,
label_group: labels,
});
}
pub fn excluding_host_labels(mut self, labels: Vec<LabelCondition>) -> Self {
self.exclude_host_labels(labels);
self
}
/// Add a service label group that must ALL match (AND).
pub fn require_service_labels(&mut self, labels: Vec<LabelCondition>) {
self.service_label_groups.push(LabelGroupCondition {
operator: LabelOperator::And,
label_group: labels,
});
}
pub fn with_service_labels(mut self, labels: Vec<LabelCondition>) -> Self {
self.require_service_labels(labels);
self
}
/// Add a service label group where ANY must match (OR).
pub fn require_any_service_label(&mut self, labels: Vec<LabelCondition>) {
self.service_label_groups.push(LabelGroupCondition {
operator: LabelOperator::Or,
label_group: labels,
});
}
pub fn with_any_service_label(mut self, labels: Vec<LabelCondition>) -> Self {
self.require_any_service_label(labels);
self
}
/// Add a service label group that must NOT match (NOT).
pub fn exclude_service_labels(&mut self, labels: Vec<LabelCondition>) {
self.service_label_groups.push(LabelGroupCondition {
operator: LabelOperator::Not,
label_group: labels,
});
}
pub fn excluding_service_labels(mut self, labels: Vec<LabelCondition>) -> Self {
self.exclude_service_labels(labels);
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct MatchCondition {
match_on: Vec<String>,
operator: MatchOperator,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum MatchOperator {
#[default]
OneOf,
NoneOf,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct HostTagCondition {
key: String,
match_on: TagMatchOperator,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub enum TagMatchOperator {
Is(String),
IsNot(String),
OneOff(Vec<String>),
NoneOff(Vec<String>),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct LabelGroupCondition {
operator: LabelOperator,
label_group: Vec<LabelCondition>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct LabelCondition {
operator: LabelOperator,
label: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub enum LabelOperator {
#[default]
And,
Or,
Not,
}
domain_client!(Rule, rule_api);
domain_create!(Rule, Rule);
domain_update!(Rule, Rule);
domain_delete!(Rule);
impl ApiClient<Rule> {
pub async fn read(&self, id: &str) -> Result<Rule> {
let mut object = self.inner.read_domain_object::<Rule, _>(id, &()).await?;
object.extensions.id = Some(Uuid::parse_str(id)?);
Ok(object.extensions)
}
pub async fn bulk_read(&self) -> Result<Vec<Rule>> {
let objects = self.inner.bulk_read_domain_objects::<Rule, _>(&()).await?;
let objects = objects
.value
.into_iter()
.map(|mut object| {
object.extensions.id = Some(Uuid::parse_str(&object.id)?);
Ok::<_, Error>(object.extensions)
})
.collect::<Result<Vec<_>>>()?;
Ok(objects)
}
}
mod serde_tags {
use serde::{
Deserialize, Serialize,
de::{self, Error, Unexpected, Visitor},
ser::SerializeStruct,
};
use crate::api::rules::{HostTagCondition, TagMatchOperator};
impl Serialize for HostTagCondition {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_struct("HostTagCondition", 3)?;
map.serialize_field("key", &self.key)?;
match &self.match_on {
TagMatchOperator::Is(value) => {
map.serialize_field("operator", "is")?;
map.serialize_field("value", &value)?;
}
TagMatchOperator::IsNot(value) => {
map.serialize_field("operator", "is_not")?;
map.serialize_field("value", &value)?;
}
TagMatchOperator::OneOff(items) => {
map.serialize_field("operator", "one_of")?;
map.serialize_field("value", items)?;
}
TagMatchOperator::NoneOff(items) => {
map.serialize_field("operator", "none_of")?;
map.serialize_field("value", items)?;
}
};
map.end()
}
}
struct HostTagConditionVisitor;
impl<'de> Visitor<'de> for HostTagConditionVisitor {
type Value = HostTagCondition;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("expected a struct with fields key, operator and value")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
enum Field {
Key,
Operator,
Value,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
enum Operator {
Is,
IsNot,
OneOff,
NoneOff,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum ValueKind {
Single(String),
Multiple(Vec<String>),
}
impl ValueKind {
fn into_single(self) -> Option<String> {
if let Self::Single(s) = self {
Some(s)
} else {
None
}
}
fn into_multiple(self) -> Result<Vec<String>, String> {
match self {
Self::Multiple(ss) => Ok(ss),
Self::Single(s) => Err(s),
}
}
}
let mut tkey: Option<String> = None;
let mut toperator: Option<Operator> = None;
let mut tvalue: Option<ValueKind> = None;
while let Some(key) = map.next_key()? {
#[allow(unused_must_use)]
match key {
Field::Key => {
tkey.insert(map.next_value()?);
}
Field::Operator => {
toperator.insert(map.next_value()?);
}
Field::Value => {
tvalue.insert(map.next_value()?);
}
};
}
let key = tkey.ok_or(A::Error::missing_field("key"))?;
let match_on = match toperator.ok_or(de::Error::missing_field("operator"))? {
Operator::Is => TagMatchOperator::Is(
tvalue
.and_then(|v| v.into_single())
.ok_or(A::Error::invalid_value(Unexpected::Seq, &"single string"))?,
),
Operator::IsNot => TagMatchOperator::IsNot(
tvalue
.and_then(|v| v.into_single())
.ok_or(A::Error::invalid_value(Unexpected::Seq, &"single string"))?,
),
Operator::OneOff => TagMatchOperator::OneOff(
tvalue
.ok_or(A::Error::missing_field("value"))?
.into_multiple()
.map_err(|s| {
A::Error::invalid_value(
Unexpected::Str(s.as_str()),
&"sequence of stirngs",
)
})?,
),
Operator::NoneOff => TagMatchOperator::NoneOff(
tvalue
.ok_or(A::Error::missing_field("value"))?
.into_multiple()
.map_err(|s| {
A::Error::invalid_value(
Unexpected::Str(s.as_str()),
&"sequence of stirngs",
)
})?,
),
};
Ok(HostTagCondition { key, match_on })
}
}
impl<'de> Deserialize<'de> for HostTagCondition {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_map(HostTagConditionVisitor)
}
}
}
-14
View File
@@ -1,14 +0,0 @@
use serde::{Deserialize, Serialize};
#[cfg(feature = "schemars")]
use schemars::JsonSchema;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct HostContactGroups {
pub groups: Vec<String>,
pub r#use: bool,
pub use_for_services: bool,
pub recurse_use: bool,
pub recurse_perms: bool,
}
-2
View File
@@ -1,2 +0,0 @@
pub mod contactgroups;
pub mod snmp;
-77
View File
@@ -1,77 +0,0 @@
use serde::{Deserialize, Serialize};
#[cfg(feature = "schemars")]
use schemars::JsonSchema;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub enum SnmpV3AuthProtocol {
#[serde(rename = "MD5-96")]
Md5,
#[serde(rename = "SHA-1-96")]
Sha96,
#[serde(rename = "SHA-2-224")]
Sha224,
#[serde(rename = "SHA-2-256")]
Sha256,
#[serde(rename = "SHA-2-384")]
Sha384,
#[serde(rename = "SHA-2-512")]
Sha512,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct SnmpV3Auth {
pub auth_protocol: SnmpV3AuthProtocol,
pub security_name: String,
pub auth_password: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub enum SnmpV3PrivProtocol {
#[serde(rename = "CBC-DES")]
Des,
#[serde(rename = "3DES-EDE")]
Des3,
#[serde(rename = "AES-128")]
Aes128,
#[serde(rename = "AES-192")]
Aes192,
#[serde(rename = "AES-256")]
Aes256,
#[serde(rename = "AES-192-Blumenthal")]
Aes192Blumenthal,
#[serde(rename = "AES-256-Blumenthal")]
Aes256Blumenthal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct SnmpV3Priv {
pub privacy_protocol: SnmpV3PrivProtocol,
pub privacy_password: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(tag = "type")]
pub enum SnmpCommunity {
V1V2Community {
community: String,
},
NoAuthNoPriv {
security_name: String,
},
AuthNoPriv {
#[serde(flatten)]
auth: SnmpV3Auth,
},
AuthPriv {
#[serde(flatten)]
authentication: SnmpV3Auth,
#[serde(flatten)]
privacy: SnmpV3Priv,
},
}
+24 -12
View File
@@ -1,10 +1,13 @@
use crate::api::Display; use crate::api::Display;
use serde::{Deserialize, Serialize};
#[cfg(feature = "schemars")] #[cfg(feature = "schemars")]
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::api::{DomainExtension, DomainType, domain_bulk_read, domain_client, domain_create, domain_delete, domain_read, domain_update}; use crate::api::{
DomainExtension, DomainType, domain_bulk_read, domain_client, domain_create, domain_delete,
domain_read, domain_update,
};
use crate::{ApiClient, Client, Result}; use crate::{ApiClient, Client, Result};
domain_client!(TagGroup, tag_api); domain_client!(TagGroup, tag_api);
@@ -15,7 +18,7 @@ pub struct ExtendedTagGroup {
pub id: String, pub id: String,
pub title: String, pub title: String,
#[serde(flatten, default)] #[serde(flatten, default)]
pub inner: TagGroup pub inner: TagGroup,
} }
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
@@ -25,7 +28,7 @@ pub struct TagGroup {
pub topic: String, pub topic: String,
#[serde(default, skip_serializing_if = "String::is_empty")] #[serde(default, skip_serializing_if = "String::is_empty")]
pub help: String, pub help: String,
pub tags: Vec<Tag> pub tags: Vec<Tag>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -33,12 +36,16 @@ pub struct TagGroup {
pub struct Tag { pub struct Tag {
pub id: Option<String>, pub id: Option<String>,
pub title: String, pub title: String,
pub aux_tags: Vec<String> pub aux_tags: Vec<String>,
} }
impl ExtendedTagGroup { impl ExtendedTagGroup {
pub fn new(id: String, title: String) -> Self { pub fn new(id: String, title: String) -> Self {
Self { id, title, inner: Default::default() } Self {
id,
title,
inner: Default::default(),
}
} }
pub fn set_topic(&mut self, topic: String) { pub fn set_topic(&mut self, topic: String) {
@@ -69,14 +76,14 @@ impl Tag {
Self { Self {
id: Some(id), id: Some(id),
title, title,
aux_tags: Vec::new() aux_tags: Vec::new(),
} }
} }
pub fn new_default(title: String) -> Self { pub fn new_default(title: String) -> Self {
Self { Self {
id: None, id: None,
title, title,
aux_tags: Vec::new() aux_tags: Vec::new(),
} }
} }
@@ -95,12 +102,16 @@ pub struct TagUpdateRequest {
title: String, title: String,
#[serde(flatten, default)] #[serde(flatten, default)]
inner: TagGroup, inner: TagGroup,
repair: bool repair: bool,
} }
impl TagUpdateRequest { impl TagUpdateRequest {
pub fn new(title: String, repair: bool) -> Self { pub fn new(title: String, repair: bool) -> Self {
Self { title, repair, inner: Default::default() } Self {
title,
repair,
inner: Default::default(),
}
} }
pub fn set_topic(&mut self, topic: String) { pub fn set_topic(&mut self, topic: String) {
@@ -130,7 +141,7 @@ impl TagUpdateRequest {
#[cfg_attr(feature = "schemars", derive(JsonSchema))] #[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub struct TagDeleteQuery { pub struct TagDeleteQuery {
repair: bool, repair: bool,
mode: Option<TagDeleteMode> mode: Option<TagDeleteMode>,
} }
impl TagDeleteQuery { impl TagDeleteQuery {
@@ -143,7 +154,8 @@ impl TagDeleteQuery {
#[cfg_attr(feature = "schemars", derive(JsonSchema))] #[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub enum TagDeleteMode { pub enum TagDeleteMode {
Abort, Abort,
#[default] Delete, #[default]
Delete,
Remove, Remove,
} }
+19 -57
View File
@@ -10,9 +10,7 @@ use serde::Serialize;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use tokio::sync::Semaphore; use tokio::sync::Semaphore;
use crate::api::{ use crate::api::{DomainCollection, DomainType};
DomainCollection, DomainObject, DomainType,
};
use crate::{Error, Result}; use crate::{Error, Result};
#[derive(Clone)] #[derive(Clone)]
@@ -224,15 +222,9 @@ impl InnerClient {
pub(crate) async fn handle_request(&self, request: Request) -> Result<String> { pub(crate) async fn handle_request(&self, request: Request) -> Result<String> {
let permit = self.semaphore.acquire().await.unwrap(); let permit = self.semaphore.acquire().await.unwrap();
debug!("sending {}-request to {}", request.method(), request.url()); debug!("sending {}-request to {}", request.method(), request.url());
trace!( if let Some(body) = request.body().as_ref().and_then(|b| b.as_bytes()) {
"with body: {}", trace!("with body: {}", String::from_utf8_lossy(body))
request }
.body()
.as_ref()
.and_then(|b| b.as_bytes())
.map(String::from_utf8_lossy)
.unwrap_or_default()
);
let response = self let response = self
.http .http
@@ -288,11 +280,11 @@ impl InnerClient {
&self, &self,
domain_type: DomainType, domain_type: DomainType,
object: &O, object: &O,
query: &Q query: &Q,
) -> Result<()> ) -> Result<()>
where where
O: Serialize, O: Serialize,
Q: Serialize Q: Serialize,
{ {
let url = self.collection_url(domain_type); let url = self.collection_url(domain_type);
let request = self let request = self
@@ -314,15 +306,10 @@ impl InnerClient {
) -> Result<O> ) -> Result<O>
where where
O: DeserializeOwned, O: DeserializeOwned,
Q: Serialize Q: Serialize,
{ {
let url = self.object_url(domain_type, id); let url = self.object_url(domain_type, id);
let request = self let request = self.http.get(url).query(query).build().unwrap();
.http
.get(url)
.query(query)
.build()
.unwrap();
self.query_api(request).await self.query_api(request).await
} }
@@ -358,12 +345,7 @@ impl InnerClient {
Q: Serialize, Q: Serialize,
{ {
let url = self.object_url(domain_type, id); let url = self.object_url(domain_type, id);
let request = self let request = self.http.delete(url).query(&query).build().unwrap();
.http
.delete(url)
.query(&query)
.build()
.unwrap();
self.invoke_api(request).await self.invoke_api(request).await
} }
@@ -399,28 +381,18 @@ impl InnerClient {
pub(crate) async fn bulk_read<O, Q>( pub(crate) async fn bulk_read<O, Q>(
&self, &self,
domain_type: DomainType, domain_type: DomainType,
query: &Q query: &Q,
) -> Result<Vec<DomainObject<O>>> ) -> Result<DomainCollection<O>>
where where
O: DeserializeOwned, O: DeserializeOwned,
Q: Serialize Q: Serialize,
{ {
let url = self.collection_url(domain_type); let url = self.collection_url(domain_type);
let request = self.http let request = self.http.get(url).query(query).build().unwrap();
.get(url)
.query(query)
.build()
.unwrap();
let response: DomainCollection<O> = self.query_api(request).await?;
Ok(response.value)
self.query_api(request).await
} }
pub(crate) async fn bulk_update<O>( pub(crate) async fn bulk_update<O>(&self, domain_type: DomainType, entries: &[O]) -> Result<()>
&self,
domain_type: DomainType,
entries: &[O],
) -> Result<()>
where where
O: Serialize, O: Serialize,
{ {
@@ -432,19 +404,14 @@ impl InnerClient {
let url = self.bulk_action_url(domain_type, BulkAction::Update); let url = self.bulk_action_url(domain_type, BulkAction::Update);
let body = Request::<O> { entries }; let body = Request::<O> { entries };
let request = self let request = self.http.put(url).json(&body).build().unwrap();
.http
.put(url)
.json(&body)
.build()
.unwrap();
self.invoke_api(request).await self.invoke_api(request).await
} }
pub(crate) async fn bulk_delete( pub(crate) async fn bulk_delete(
&self, &self,
domain_type: DomainType, domain_type: DomainType,
entries: &[String] entries: &[String],
) -> Result<()> { ) -> Result<()> {
#[derive(Serialize)] #[derive(Serialize)]
struct Request<'a> { struct Request<'a> {
@@ -454,12 +421,7 @@ impl InnerClient {
let url = self.bulk_action_url(domain_type, BulkAction::Delete); let url = self.bulk_action_url(domain_type, BulkAction::Delete);
let body = Request { entries }; let body = Request { entries };
let request = self let request = self.http.post(url).json(&body).build().unwrap();
.http
.post(url)
.json(&body)
.build()
.unwrap();
self.invoke_api(request).await self.invoke_api(request).await
} }
@@ -488,7 +450,7 @@ pub struct ApiClient<D> {
_marker: PhantomData<D>, _marker: PhantomData<D>,
} }
impl <D> ApiClient<D> { impl<D> ApiClient<D> {
pub(crate) fn new(inner: &Client) -> Self { pub(crate) fn new(inner: &Client) -> Self {
Self { Self {
inner: inner.0.clone(), inner: inner.0.clone(),
+2
View File
@@ -32,6 +32,8 @@ pub enum Error {
MissingHeader(&'static str), MissingHeader(&'static str),
#[error("Failed to parse ETag header: {0}")] #[error("Failed to parse ETag header: {0}")]
ParseEtag(#[source] std::string::FromUtf8Error), ParseEtag(#[source] std::string::FromUtf8Error),
#[error("failed to parse domain object id: {0}")]
ParseId(#[from] uuid::Error),
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
+146 -24
View File
@@ -8,13 +8,11 @@ const PASSWORD: &str = "C5t71DUPAu2D";
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
let log_config = simplelog::ConfigBuilder::new()
.set_location_level(simplelog::LevelFilter::Error)
.build();
simplelog::TermLogger::init( simplelog::TermLogger::init(
simplelog::LevelFilter::Trace, simplelog::LevelFilter::Trace,
log_config, simplelog::ConfigBuilder::new()
.set_location_level(simplelog::LevelFilter::Error)
.build(),
simplelog::TerminalMode::Stderr, simplelog::TerminalMode::Stderr,
simplelog::ColorChoice::Auto, simplelog::ColorChoice::Auto,
) )
@@ -33,6 +31,8 @@ async fn main() -> Result<()> {
test_host_groups(client.clone()).await?; test_host_groups(client.clone()).await?;
test_service_groups(client.clone()).await?; test_service_groups(client.clone()).await?;
test_contact_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"); info!("tests done; all pass");
Ok(()) Ok(())
@@ -92,8 +92,8 @@ async fn test_folders(client: Client) -> Result<()> {
} }
async fn test_hosts(client: Client) -> Result<()> { async fn test_hosts(client: Client) -> Result<()> {
use checkmk_api::api::hosts::*;
use checkmk_api::api::folders::FolderAttributes; use checkmk_api::api::folders::FolderAttributes;
use checkmk_api::api::hosts::*;
const TESTHOST: &str = "test-host"; const TESTHOST: &str = "test-host";
info!("testing hosts"); info!("testing hosts");
@@ -101,17 +101,17 @@ async fn test_hosts(client: Client) -> Result<()> {
let creq = HostCreationRequest::new( let creq = HostCreationRequest::new(
TESTHOST.to_string(), TESTHOST.to_string(),
"~".to_string(), "~".to_string(),
FolderAttributes::default().with_site("dev".to_string()) FolderAttributes::default().with_site("dev".to_string()),
); );
let cquery = HostCreationQuery::new(false); let cquery = HostCreationQuery::new(false);
let rquery = HostReadQuery::new(true); let rquery = HostReadQuery::new(true);
let ureq1 = HostUpdateRequest::replace( let ureq1 = HostUpdateRequest::replace(
FolderAttributes::default() FolderAttributes::default()
.with_site("dev".to_string()) .with_site("dev".to_string())
.with_label("environment".to_string(), "test".to_string()) .with_label("environment".to_string(), "test".to_string()),
); );
let ureq2 = HostUpdateRequest::update( let ureq2 = HostUpdateRequest::update(
FolderAttributes::default().with_label("purpose".to_string(), "testing".to_string()) FolderAttributes::default().with_label("purpose".to_string(), "testing".to_string()),
); );
info!("creating test host"); info!("creating test host");
@@ -210,7 +210,10 @@ async fn test_host_groups(client: Client) -> Result<()> {
let api = client.host_group_api(); let api = client.host_group_api();
info!("creating test host group"); info!("creating test host group");
api.create(&HostGroup::new(TESTGROUP.to_string(), "Test Host Group".to_string())) api.create(&HostGroup::new(
TESTGROUP.to_string(),
"Test Host Group".to_string(),
))
.await .await
.inspect_err(|e| error!("failed to create host group: {e}"))?; .inspect_err(|e| error!("failed to create host group: {e}"))?;
@@ -227,8 +230,14 @@ async fn test_host_groups(client: Client) -> Result<()> {
info!("bulk creating additional host groups"); info!("bulk creating additional host groups");
api.bulk_create(&[ api.bulk_create(&[
HostGroup::new("test-host-group-2".to_string(), "Test Host Group 2".to_string()), HostGroup::new(
HostGroup::new("test-host-group-3".to_string(), "Test Host Group 3".to_string()), "test-host-group-2".to_string(),
"Test Host Group 2".to_string(),
),
HostGroup::new(
"test-host-group-3".to_string(),
"Test Host Group 3".to_string(),
),
]) ])
.await .await
.inspect_err(|e| error!("failed to bulk create host groups: {e}"))?; .inspect_err(|e| error!("failed to bulk create host groups: {e}"))?;
@@ -241,8 +250,14 @@ async fn test_host_groups(client: Client) -> Result<()> {
info!("bulk updating host groups"); info!("bulk updating host groups");
api.bulk_update(&[ api.bulk_update(&[
Group::new("test-host-group-2".to_string(), "Updated Host Group 2".to_string()), Group::new(
Group::new("test-host-group-3".to_string(), "Updated Host Group 3".to_string()), "test-host-group-2".to_string(),
"Updated Host Group 2".to_string(),
),
Group::new(
"test-host-group-3".to_string(),
"Updated Host Group 3".to_string(),
),
]) ])
.await .await
.inspect_err(|e| error!("failed to bulk update host groups: {e}"))?; .inspect_err(|e| error!("failed to bulk update host groups: {e}"))?;
@@ -271,7 +286,10 @@ async fn test_service_groups(client: Client) -> Result<()> {
let api = client.service_group_api(); let api = client.service_group_api();
info!("creating test service group"); info!("creating test service group");
api.create(&ServiceGroup::new(TESTGROUP.to_string(), "Test Service Group".to_string())) api.create(&ServiceGroup::new(
TESTGROUP.to_string(),
"Test Service Group".to_string(),
))
.await .await
.inspect_err(|e| error!("failed to create service group: {e}"))?; .inspect_err(|e| error!("failed to create service group: {e}"))?;
@@ -288,8 +306,14 @@ async fn test_service_groups(client: Client) -> Result<()> {
info!("bulk creating additional service groups"); info!("bulk creating additional service groups");
api.bulk_create(&[ api.bulk_create(&[
ServiceGroup::new("test-service-group-2".to_string(), "Test Service Group 2".to_string()), ServiceGroup::new(
ServiceGroup::new("test-service-group-3".to_string(), "Test Service Group 3".to_string()), "test-service-group-2".to_string(),
"Test Service Group 2".to_string(),
),
ServiceGroup::new(
"test-service-group-3".to_string(),
"Test Service Group 3".to_string(),
),
]) ])
.await .await
.inspect_err(|e| error!("failed to bulk create service groups: {e}"))?; .inspect_err(|e| error!("failed to bulk create service groups: {e}"))?;
@@ -302,8 +326,14 @@ async fn test_service_groups(client: Client) -> Result<()> {
info!("bulk updating service groups"); info!("bulk updating service groups");
api.bulk_update(&[ api.bulk_update(&[
Group::new("test-service-group-2".to_string(), "Updated Service Group 2".to_string()), Group::new(
Group::new("test-service-group-3".to_string(), "Updated Service Group 3".to_string()), "test-service-group-2".to_string(),
"Updated Service Group 2".to_string(),
),
Group::new(
"test-service-group-3".to_string(),
"Updated Service Group 3".to_string(),
),
]) ])
.await .await
.inspect_err(|e| error!("failed to bulk update service groups: {e}"))?; .inspect_err(|e| error!("failed to bulk update service groups: {e}"))?;
@@ -324,6 +354,83 @@ async fn test_service_groups(client: Client) -> Result<()> {
Ok(()) Ok(())
} }
async fn test_activations(client: Client) -> Result<()> {
use checkmk_api::api::activations::*;
info!("testing activations");
let api = client.activation_api();
let pending = api
.read_pending_changes()
.await
.inspect_err(|e| error!("failed to read pending changes: {e}"))?;
info!("pending changes: {pending:#?}");
let running = api
.read_running_activations()
.await
.inspect_err(|e| error!("failed to read running activations: {e}"))?;
info!("running activations: {running:#?}");
let options = ActivationOptions::default()
.with_redirect(false)
.with_force_foreign_changes(false)
.with_site(SITENAME.to_string());
info!("activating pending changes");
let activation_id = api
.activate_pending_changes(&options)
.await
.inspect_err(|e| error!("failed to activate pending changes: {e}"))?;
info!("activation started with id: {activation_id}");
info!("waiting for activation {activation_id} to complete");
api.await_completion(&activation_id)
.await
.inspect_err(|e| error!("failed to await activation completion: {e}"))?;
info!("activation {activation_id} completed");
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<()> { async fn test_contact_groups(client: Client) -> Result<()> {
use checkmk_api::api::groups::*; use checkmk_api::api::groups::*;
const TESTGROUP: &str = "test-contact-group"; const TESTGROUP: &str = "test-contact-group";
@@ -332,7 +439,10 @@ async fn test_contact_groups(client: Client) -> Result<()> {
let api = client.contact_group_api(); let api = client.contact_group_api();
info!("creating test contact group"); info!("creating test contact group");
api.create(&ContactGroup::new(TESTGROUP.to_string(), "Test Contact Group".to_string())) api.create(&ContactGroup::new(
TESTGROUP.to_string(),
"Test Contact Group".to_string(),
))
.await .await
.inspect_err(|e| error!("failed to create contact group: {e}"))?; .inspect_err(|e| error!("failed to create contact group: {e}"))?;
@@ -349,8 +459,14 @@ async fn test_contact_groups(client: Client) -> Result<()> {
info!("bulk creating additional contact groups"); info!("bulk creating additional contact groups");
api.bulk_create(&[ api.bulk_create(&[
ContactGroup::new("test-contact-group-2".to_string(), "Test Contact Group 2".to_string()), ContactGroup::new(
ContactGroup::new("test-contact-group-3".to_string(), "Test Contact Group 3".to_string()), "test-contact-group-2".to_string(),
"Test Contact Group 2".to_string(),
),
ContactGroup::new(
"test-contact-group-3".to_string(),
"Test Contact Group 3".to_string(),
),
]) ])
.await .await
.inspect_err(|e| error!("failed to bulk create contact groups: {e}"))?; .inspect_err(|e| error!("failed to bulk create contact groups: {e}"))?;
@@ -363,8 +479,14 @@ async fn test_contact_groups(client: Client) -> Result<()> {
info!("bulk updating contact groups"); info!("bulk updating contact groups");
api.bulk_update(&[ api.bulk_update(&[
Group::new("test-contact-group-2".to_string(), "Updated Contact Group 2".to_string()), Group::new(
Group::new("test-contact-group-3".to_string(), "Updated Contact Group 3".to_string()), "test-contact-group-2".to_string(),
"Updated Contact Group 2".to_string(),
),
Group::new(
"test-contact-group-3".to_string(),
"Updated Contact Group 3".to_string(),
),
]) ])
.await .await
.inspect_err(|e| error!("failed to bulk update contact groups: {e}"))?; .inspect_err(|e| error!("failed to bulk update contact groups: {e}"))?;