use super::{
Host, HostRef, HostTemplate, HostTemplateRef, MonitoringCluster, MonitoringClusterRef,
};
use crate::{prelude::*, util::*};
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct BSMComponent {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub host_template: Option<HostTemplateRef>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_string_or_number_to_u64",
default
)]
pub host_template_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hosts: Option<ConfigRefMap<HostRef>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quorum_pct: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub monitoring_cluster: Option<MonitoringClusterRef>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_string_or_number_to_u64",
default
)]
pub has_icon: Option<u64>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_string_or_number_to_u64",
default
)]
pub id: Option<u64>,
#[serde(
rename = "ref",
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_readonly",
default
)]
pub ref_: Option<String>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_string_or_number_to_option_bool",
serialize_with = "serialize_option_bool_as_string",
default
)]
pub uncommitted: Option<bool>,
}
impl CreateFromJson for BSMComponent {}
impl ConfigObject for BSMComponent {
type Builder = BSMComponentBuilder;
fn builder() -> Self::Builder {
BSMComponentBuilder::new()
}
fn config_path() -> Option<String> {
Some("/config/bsmcomponent".to_string())
}
fn minimal(name: &str) -> Result<Self, OpsviewConfigError> {
Ok(Self {
name: validate_and_trim_bsmcomponent_name(name)?,
..Default::default()
})
}
fn unique_name(&self) -> String {
let name = self.name.clone();
match (self.id.as_ref(), self.ref_.as_ref()) {
(Some(id), _) => format!("{}-{}", name, id),
(_, Some(ref_)) => ref_.clone(),
_ => name,
}
}
}
impl Persistent for BSMComponent {
fn id(&self) -> Option<u64> {
self.id
}
fn ref_(&self) -> Option<String> {
if self.ref_.as_ref().is_some_and(|x| !x.is_empty()) {
self.ref_.clone()
} else {
None
}
}
fn name(&self) -> Option<String> {
if self.name.is_empty() {
None
} else {
Some(self.name.clone())
}
}
fn name_regex(&self) -> Option<String> {
Some(BSM_COMPONENT_NAME_REGEX_STR.to_string())
}
fn validated_name(&self, name: &str) -> Result<String, OpsviewConfigError> {
validate_and_trim_bsmcomponent_name(name)
}
fn set_name(&mut self, new_name: &str) -> Result<String, OpsviewConfigError> {
self.name = self.validated_name(new_name)?;
Ok(self.name.clone())
}
fn clear_readonly(&mut self) {
self.has_icon = None;
self.id = None;
self.ref_ = None;
self.uncommitted = None;
}
}
impl PersistentMap for ConfigObjectMap<BSMComponent> {
fn config_path() -> Option<String> {
Some("/config/bsmcomponent".to_string())
}
}
#[derive(Clone, Debug, Default)]
pub struct BSMComponentBuilder {
name: Option<String>,
host_template: Option<HostTemplateRef>,
host_template_id: Option<u64>,
hosts: Option<ConfigRefMap<HostRef>>,
quorum_pct: Option<String>,
monitoring_cluster: Option<MonitoringClusterRef>,
}
impl Builder for BSMComponentBuilder {
type ConfigObject = BSMComponent;
fn new() -> Self {
BSMComponentBuilder::default()
}
fn name(mut self, name: &str) -> Self {
self.name = Some(name.to_string());
self
}
fn build(self) -> Result<Self::ConfigObject, OpsviewConfigError> {
let name = require_field(&self.name, "name")?;
let host_template = require_field(&self.host_template, "host_template")?;
let hosts = require_field(&self.hosts, "hosts")?;
let quorum_pct = require_field(&self.quorum_pct, "quorum_pct")?;
Ok(BSMComponent {
name: validate_and_trim_bsmcomponent_name(&name)?,
host_template: Some(host_template),
host_template_id: self.host_template_id,
monitoring_cluster: self.monitoring_cluster,
quorum_pct: Some(validated_pct_and_ratio(&quorum_pct, hosts.len())?),
hosts: Some(hosts),
has_icon: None,
id: None,
ref_: None,
uncommitted: None,
})
}
}
impl BSMComponentBuilder {
pub fn clear_host_template(mut self) -> Self {
self.host_template = None;
self
}
pub fn clear_host_template_id(mut self) -> Self {
self.host_template_id = None;
self
}
pub fn clear_hosts(mut self) -> Self {
self.hosts = None;
self
}
pub fn clear_monitoring_cluster(mut self) -> Self {
self.monitoring_cluster = None;
self
}
pub fn clear_name(mut self) -> Self {
self.name = None;
self
}
pub fn clear_quorum_pct(mut self) -> Self {
self.quorum_pct = None;
self
}
pub fn host_template(mut self, host_template: HostTemplate) -> Self {
self.host_template = Some(HostTemplateRef::from(host_template));
self
}
pub fn host_template_id(mut self, host_template_id: u64) -> Self {
self.host_template_id = Some(host_template_id);
self
}
pub fn hosts(mut self, hosts: &ConfigObjectMap<Host>) -> Self {
if let Some(ref host_template) = self.host_template {
for host in hosts.values() {
if !host.has_template(host_template) {
panic!(
"Host '{}' does not have the template '{}'",
host.name,
host_template.name()
);
}
}
} else {
panic!("host_template must be set before hosts");
}
self.hosts = Some(hosts.into());
self
}
pub fn monitoring_cluster(mut self, monitoring_cluster: MonitoringCluster) -> Self {
self.monitoring_cluster = Some(MonitoringClusterRef::from(monitoring_cluster));
self
}
pub fn quorum_pct(mut self, quorum_pct: &str) -> Self {
self.quorum_pct = Some(quorum_pct.to_string());
self
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
pub struct BSMComponentRef {
name: String,
#[serde(
rename = "ref",
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_readonly",
default
)]
ref_: Option<String>,
}
impl CreateFromJson for BSMComponentRef {}
impl ConfigRef for BSMComponentRef {
type FullObject = BSMComponent;
fn ref_(&self) -> Option<String> {
self.ref_.clone()
}
fn name(&self) -> String {
self.name.clone()
}
fn unique_name(&self) -> String {
let name = self.name.clone();
match self.ref_.as_ref() {
Some(ref_) => ref_.clone(),
_ => name,
}
}
}
impl From<BSMComponent> for BSMComponentRef {
fn from(component: BSMComponent) -> Self {
Self {
name: component.name.clone(),
ref_: component.ref_.clone(),
}
}
}
impl From<Arc<BSMComponent>> for BSMComponentRef {
fn from(item: Arc<BSMComponent>) -> Self {
let component: BSMComponent = Arc::try_unwrap(item).unwrap_or_else(|arc| (*arc).clone());
BSMComponentRef::from(component)
}
}
impl From<&ConfigObjectMap<BSMComponent>> for ConfigRefMap<BSMComponentRef> {
fn from(components: &ConfigObjectMap<BSMComponent>) -> Self {
ref_map_from(components)
}
}
lazy_static! {
static ref QUORUM_PCT_REGEX: Regex = regex::Regex::new(r"^\d{1,3}\.\d{2}$").unwrap();
}
fn validated_pct_and_ratio(
percentage: &str,
number_of_hosts: usize,
) -> Result<String, OpsviewConfigError> {
if percentage == "0.00" {
return Ok(percentage.to_string());
}
if percentage == "100.00" {
return Ok(percentage.to_string());
}
if number_of_hosts == 0 {
return Err(OpsviewConfigError::InvalidQuorum(
"The number of hosts must be greater than 0".to_string(),
));
}
if !QUORUM_PCT_REGEX.is_match(percentage) {
return Err(OpsviewConfigError::InvalidQuorum(
"Must be a number with exactly 2 decimals".to_string(),
));
}
let mut valid_percentages = Vec::new();
for host_count in 0..=number_of_hosts {
let pct = 100.0 * host_count as f64 / number_of_hosts as f64;
valid_percentages.push(format!("{:.2}", pct));
}
if valid_percentages.contains(&percentage.to_string()) {
Ok(percentage.to_string())
} else {
Err(OpsviewConfigError::InvalidQuorum(format!(
"The percentage '{}' is not a valid ratio for '{}' hosts",
percentage, number_of_hosts
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{HostGroup, MonitoringCluster};
use pretty_assertions::assert_eq;
#[test]
fn test_is_valid_pct_and_ratio() {
assert!(validated_pct_and_ratio("100.00", 1).is_ok());
assert!(validated_pct_and_ratio("99.99", 10000).is_ok());
assert!(validated_pct_and_ratio("0.00", 0).is_ok());
assert!(validated_pct_and_ratio("0.01", 10000).is_ok());
assert!(validated_pct_and_ratio("0.99", 10000).is_ok());
assert!(validated_pct_and_ratio("1.00", 100).is_ok());
assert!(validated_pct_and_ratio("1.01", 10000).is_ok());
assert!(validated_pct_and_ratio("99.99", 10000).is_ok());
assert!(validated_pct_and_ratio("100.00", 1).is_ok());
assert!(validated_pct_and_ratio("50.00", 0).is_err());
assert!(validated_pct_and_ratio("100.01", 10000).is_err());
assert!(validated_pct_and_ratio("999.99", 1000).is_err());
assert!(validated_pct_and_ratio("999.99", 10).is_err());
assert!(validated_pct_and_ratio("999.99", 100).is_err());
assert!(validated_pct_and_ratio("100", 1).is_err());
assert!(validated_pct_and_ratio("1", 100).is_err());
assert!(validated_pct_and_ratio("0.00", 3).is_ok()); assert!(validated_pct_and_ratio("100.00", 3).is_ok()); assert!(validated_pct_and_ratio("33.33", 3).is_ok()); assert!(validated_pct_and_ratio("66.67", 3).is_ok()); assert!(validated_pct_and_ratio("100", 3).is_err());
assert!(validated_pct_and_ratio("0.00", 2).is_ok());
assert!(validated_pct_and_ratio("50.00", 2).is_ok());
assert!(validated_pct_and_ratio("100.00", 2).is_ok());
assert!(validated_pct_and_ratio("90.00", 2).is_err());
let host_template = HostTemplate::builder()
.name("Host Template ")
.build()
.unwrap();
let mut host_templates = ConfigObjectMap::<HostTemplate>::new();
host_templates.add(host_template.clone());
let host_templates = host_templates;
let root_hostgroup = HostGroup::builder()
.name("Opsview")
.clear_parent()
.build()
.unwrap();
let cluster = MonitoringCluster::minimal("Cluster 1")
.expect("Failed to create cluster with name 'Cluster 1'");
let host = Host::builder()
.name("Host_1")
.alias("Host 1")
.ip("127.0.0.1")
.hostgroup(root_hostgroup)
.monitored_by(cluster)
.hosttemplates(&host_templates)
.build()
.unwrap();
let mut hosts = ConfigObjectMap::<Host>::new();
hosts.add(host);
let bsm_comp_1 = BSMComponent::builder()
.name("Comp 1")
.host_template(host_template.clone())
.hosts(&hosts)
.quorum_pct("100.00")
.build();
assert!(bsm_comp_1.is_ok());
let bsm_comp_2 = BSMComponent::builder()
.name("Comp 1")
.host_template(host_template.clone())
.hosts(&hosts)
.quorum_pct("100")
.build();
assert!(bsm_comp_2.is_err());
assert_eq!(
bsm_comp_2.err().unwrap().to_string(),
"Invalid quorum: Must be a number with exactly 2 decimals",
);
let bsm_comp_3 = BSMComponent::builder()
.name("Comp 1")
.host_template(host_template)
.hosts(&hosts)
.quorum_pct("90.00")
.build();
assert!(bsm_comp_3.is_err());
assert_eq!(
bsm_comp_3.err().unwrap().to_string(),
"Invalid quorum: The percentage '90.00' is not a valid ratio for '1' hosts",
);
}
#[test]
fn test_default() {
let bsm_component = BSMComponent::default();
assert!(bsm_component.name.is_empty());
}
#[test]
fn test_minimal() {
let bsm_component = BSMComponent::minimal("My BSM Component");
assert_eq!(bsm_component.unwrap().name, "My BSM Component".to_string());
}
#[test]
fn test_is_valid_bsmcomponent_name() {
assert!(validate_and_trim_bsmcomponent_name("ValidComponent123").is_ok());
assert!(validate_and_trim_bsmcomponent_name("Valid_Component-With.Symbols!").is_ok());
assert!(validate_and_trim_bsmcomponent_name("A").is_ok());
assert!(validate_and_trim_bsmcomponent_name(
"A component name with spaces and symbols *&^%$#@!"
)
.is_ok());
assert!(validate_and_trim_bsmcomponent_name(&"a".repeat(255)).is_ok());
assert!(validate_and_trim_bsmcomponent_name("").is_err()); assert!(validate_and_trim_bsmcomponent_name(" ").is_err()); assert!(validate_and_trim_bsmcomponent_name(&"a".repeat(256)).is_err()); assert!(validate_and_trim_bsmcomponent_name("Invalid\nComponent").is_err()); assert!(validate_and_trim_bsmcomponent_name("Invalid\tComponent").is_err()); assert!(validate_and_trim_bsmcomponent_name("Invalid\rComponent").is_err());
}
}