Compare commits

..

No commits in common. "71b1d3411d4b2b0a74b707337a777a3aaa6b8ab4" and "f08b37cb7d0b432464216bbaa43348d078d567f8" have entirely different histories.

7 changed files with 61 additions and 251 deletions

12
Cargo.lock generated
View File

@ -152,7 +152,6 @@ dependencies = [
"simplelog", "simplelog",
"thiserror", "thiserror",
"tokio", "tokio",
"uuid",
] ]
[[package]] [[package]]
@ -1885,17 +1884,6 @@ 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"

View File

@ -19,7 +19,6 @@ 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"]

View File

@ -1,72 +1,20 @@
use std::fmt::Display;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{ApiClient, Client, Result}; use crate::{ApiClient, Client, Result};
use crate::api::{DomainCollection, DomainExtension, DomainObject, domain_client, domain_read};
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 ActivationStatus { pub struct ChangeActivation;
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>,
}
#[derive(Debug, Clone, Serialize, Deserialize)] impl From<&Client> for ApiClient<ChangeActivation> {
#[cfg_attr(feature = "schemars", derive(JsonSchema))] fn from(value: &Client) -> Self {
pub struct SiteStatus { Self::new(value)
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)]
@ -77,119 +25,20 @@ pub struct ActivateChangeRequest {
force_foreign_changes: bool force_foreign_changes: bool
} }
#[derive(Debug, Default, Clone, Serialize, Deserialize)] impl ApiClient<ChangeActivation> {
#[cfg_attr(feature = "schemars", derive(JsonSchema))] pub async fn activate(&self) -> Result<()> {
pub struct ActivationOptions { todo!()
redirect: bool,
sites: Vec<String>,
force_foreign_changes: bool
}
impl ActivationOptions {
pub fn set_redirect(&mut self, redirect: bool) {
self.redirect = redirect
} }
pub fn with_redirect(mut self, redirect: bool) -> Self { pub async fn await_activation(&self, activation_id: &str) -> Result<()> {
self.set_redirect(redirect); todo!()
self
} }
pub fn set_force_foreign_changes(&mut self, force: bool) { pub async fn read_current_activations(&self) -> Result<Vec<ChangeActivation>> {
self.force_foreign_changes = force; todo!()
} }
pub fn with_force_foreign_changes(mut self, force: bool) -> Self { pub async fn bulk_read_pending_changes(&self) -> Result<Vec<PendingChange>> {
self.set_force_foreign_changes(force); todo!()
self
} }
pub async fn read_activation_status(&self, activation_id: &str) -> Result<ChangeActivation> {
pub fn set_sites(&mut self, sites: Vec<String>) { todo!()
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)
} }
} }

View File

@ -5,10 +5,24 @@ use serde::{Deserialize, Serialize};
use schemars::JsonSchema; use schemars::JsonSchema;
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 {
@ -30,11 +44,10 @@ impl From<DomainObject<serde_json::Value>> for Group {
macro_rules! domain_group { macro_rules! domain_group {
($id:ident, $typ:expr, $func:ident) => { ($id:ident, $typ:expr) => {
#[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;
@ -56,6 +69,12 @@ 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
@ -81,13 +100,12 @@ 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 collection: DomainCollection<serde_json::Value> = let entries: Vec<DomainObject<serde_json::Value>> =
self.inner.bulk_read($typ, &()).await?; self.inner.bulk_read($typ, &()).await?;
let groups = collection.value let objects = entries.into_iter()
.into_iter()
.map($id::from) .map($id::from)
.collect(); .collect();
Ok(groups) Ok(objects)
} }
pub async fn bulk_update(&self, entries: &[Group]) -> Result<()> { pub async fn bulk_update(&self, entries: &[Group]) -> Result<()> {
#[derive(Serialize)] #[derive(Serialize)]
@ -122,6 +140,6 @@ macro_rules! domain_group {
} }
} }
domain_group!(HostGroup, DomainType::HostGroupConfig, host_group_api); domain_group!(HostGroup, DomainType::HostGroupConfig);
domain_group!(ServiceGroup, DomainType::ServiceGroupConfig, service_group_api); domain_group!(ServiceGroup, DomainType::ServiceGroupConfig);
domain_group!(ContactGroup, DomainType::ContactGroupConfig, contact_group_api); domain_group!(ContactGroup, DomainType::ContactGroupConfig);

View File

@ -21,8 +21,7 @@ pub enum DomainType {
ContactGroupConfig, ContactGroupConfig,
HostGroupConfig, HostGroupConfig,
ServiceGroupConfig, ServiceGroupConfig,
HostTagGroup, HostTagGroup
ActivationRun,
} }
impl fmt::Display for DomainType { impl fmt::Display for DomainType {
@ -35,7 +34,6 @@ 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")
} }
} }
} }
@ -91,15 +89,6 @@ 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;
} }
@ -166,7 +155,7 @@ impl InnerClient {
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<DomainCollection<E>> ) -> Result<Vec<DomainObject<E>>>
where where
E: DomainExtension, E: DomainExtension,
Q: Serialize Q: Serialize
@ -304,16 +293,22 @@ 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 collection = self.inner.bulk_read_domain_objects::<$id, _>(&()).await?; let objs = self.inner.bulk_read_domain_objects::<$id, _>(&()).await?;
Ok(collection.into()) let objs = objs.into_iter()
.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 collection = self.inner.bulk_read_domain_objects::<$id, _>(&query).await?; let objs = self.inner.bulk_read_domain_objects::<$id, _>(&query).await?;
Ok(collection.into()) let objs = objs.into_iter()
.map(|obj| obj.extensions)
.collect();
Ok(objs)
} }
} }
}; };

View File

@ -11,7 +11,7 @@ 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};
@ -400,7 +400,7 @@ impl InnerClient {
&self, &self,
domain_type: DomainType, domain_type: DomainType,
query: &Q query: &Q
) -> Result<DomainCollection<O>> ) -> Result<Vec<DomainObject<O>>>
where where
O: DeserializeOwned, O: DeserializeOwned,
Q: Serialize Q: Serialize
@ -412,7 +412,8 @@ impl InnerClient {
.build() .build()
.unwrap(); .unwrap();
self.query_api(request).await let response: DomainCollection<O> = self.query_api(request).await?;
Ok(response.value)
} }
pub(crate) async fn bulk_update<O>( pub(crate) async fn bulk_update<O>(

View File

@ -33,7 +33,6 @@ 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_activations(client.clone()).await?;
info!("tests done; all pass"); info!("tests done; all pass");
Ok(()) Ok(())
@ -325,45 +324,6 @@ 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_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";