1use super::{
9 Host, HostRef, HostTemplate, HostTemplateRef, MonitoringCluster, MonitoringClusterRef,
10};
11use crate::{prelude::*, util::*};
12use lazy_static::lazy_static;
13use regex::Regex;
14use serde::{Deserialize, Serialize};
15use std::sync::Arc;
16
17#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
83pub struct BSMComponent {
84 pub name: String,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
93 pub host_template: Option<HostTemplateRef>,
94
95 #[serde(
97 skip_serializing_if = "Option::is_none",
98 deserialize_with = "deserialize_string_or_number_to_u64",
99 default
100 )]
101 pub host_template_id: Option<u64>,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub hosts: Option<ConfigRefMap<HostRef>>,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
119 pub quorum_pct: Option<String>,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
125 pub monitoring_cluster: Option<MonitoringClusterRef>,
126
127 #[serde(
130 skip_serializing_if = "Option::is_none",
131 deserialize_with = "deserialize_string_or_number_to_u64",
132 default
133 )]
134 pub has_icon: Option<u64>,
135
136 #[serde(
138 skip_serializing_if = "Option::is_none",
139 deserialize_with = "deserialize_string_or_number_to_u64",
140 default
141 )]
142 pub id: Option<u64>,
143
144 #[serde(
146 rename = "ref",
147 skip_serializing_if = "Option::is_none",
148 deserialize_with = "deserialize_readonly",
149 default
150 )]
151 pub ref_: Option<String>,
152
153 #[serde(
155 skip_serializing_if = "Option::is_none",
156 deserialize_with = "deserialize_string_or_number_to_option_bool",
157 serialize_with = "serialize_option_bool_as_string",
158 default
159 )]
160 pub uncommitted: Option<bool>,
161}
162
163impl CreateFromJson for BSMComponent {}
168
169impl ConfigObject for BSMComponent {
174 type Builder = BSMComponentBuilder;
175
176 fn builder() -> Self::Builder {
181 BSMComponentBuilder::new()
182 }
183
184 fn config_path() -> Option<String> {
189 Some("/config/bsmcomponent".to_string())
190 }
191
192 fn minimal(name: &str) -> Result<Self, OpsviewConfigError> {
201 Ok(Self {
202 name: validate_and_trim_bsmcomponent_name(name)?,
203 ..Default::default()
204 })
205 }
206
207 fn unique_name(&self) -> String {
222 let name = self.name.clone();
223 match (self.id.as_ref(), self.ref_.as_ref()) {
224 (Some(id), _) => format!("{}-{}", name, id),
225 (_, Some(ref_)) => ref_.clone(),
226 _ => name,
227 }
228 }
229}
230
231impl Persistent for BSMComponent {
232 fn id(&self) -> Option<u64> {
234 self.id
235 }
236
237 fn ref_(&self) -> Option<String> {
239 if self.ref_.as_ref().is_some_and(|x| !x.is_empty()) {
240 self.ref_.clone()
241 } else {
242 None
243 }
244 }
245 fn name(&self) -> Option<String> {
247 if self.name.is_empty() {
248 None
249 } else {
250 Some(self.name.clone())
251 }
252 }
253
254 fn name_regex(&self) -> Option<String> {
255 Some(BSM_COMPONENT_NAME_REGEX_STR.to_string())
256 }
257
258 fn validated_name(&self, name: &str) -> Result<String, OpsviewConfigError> {
259 validate_and_trim_bsmcomponent_name(name)
260 }
261
262 fn set_name(&mut self, new_name: &str) -> Result<String, OpsviewConfigError> {
263 self.name = self.validated_name(new_name)?;
264 Ok(self.name.clone())
265 }
266
267 fn clear_readonly(&mut self) {
269 self.has_icon = None;
270 self.id = None;
271 self.ref_ = None;
272 self.uncommitted = None;
273 }
274}
275
276impl PersistentMap for ConfigObjectMap<BSMComponent> {
277 fn config_path() -> Option<String> {
278 Some("/config/bsmcomponent".to_string())
279 }
280}
281
282#[derive(Clone, Debug, Default)]
286pub struct BSMComponentBuilder {
287 name: Option<String>,
288 host_template: Option<HostTemplateRef>,
289 host_template_id: Option<u64>,
290 hosts: Option<ConfigRefMap<HostRef>>,
291 quorum_pct: Option<String>,
292 monitoring_cluster: Option<MonitoringClusterRef>,
293}
294
295impl Builder for BSMComponentBuilder {
296 type ConfigObject = BSMComponent;
297
298 fn new() -> Self {
303 BSMComponentBuilder::default()
304 }
305
306 fn name(mut self, name: &str) -> Self {
312 self.name = Some(name.to_string());
313 self
314 }
315
316 fn build(self) -> Result<Self::ConfigObject, OpsviewConfigError> {
326 let name = require_field(&self.name, "name")?;
327 let host_template = require_field(&self.host_template, "host_template")?;
328 let hosts = require_field(&self.hosts, "hosts")?;
329 let quorum_pct = require_field(&self.quorum_pct, "quorum_pct")?;
330
331 Ok(BSMComponent {
334 name: validate_and_trim_bsmcomponent_name(&name)?,
336 host_template: Some(host_template),
337 host_template_id: self.host_template_id,
338 monitoring_cluster: self.monitoring_cluster,
339 quorum_pct: Some(validated_pct_and_ratio(&quorum_pct, hosts.len())?),
340 hosts: Some(hosts),
341 has_icon: None,
343 id: None,
344 ref_: None,
345 uncommitted: None,
346 })
347 }
348}
349
350impl BSMComponentBuilder {
351 pub fn clear_host_template(mut self) -> Self {
353 self.host_template = None;
354 self
355 }
356
357 pub fn clear_host_template_id(mut self) -> Self {
359 self.host_template_id = None;
360 self
361 }
362
363 pub fn clear_hosts(mut self) -> Self {
365 self.hosts = None;
366 self
367 }
368
369 pub fn clear_monitoring_cluster(mut self) -> Self {
371 self.monitoring_cluster = None;
372 self
373 }
374
375 pub fn clear_name(mut self) -> Self {
377 self.name = None;
378 self
379 }
380
381 pub fn clear_quorum_pct(mut self) -> Self {
383 self.quorum_pct = None;
384 self
385 }
386
387 pub fn host_template(mut self, host_template: HostTemplate) -> Self {
393 self.host_template = Some(HostTemplateRef::from(host_template));
394 self
395 }
396
397 pub fn host_template_id(mut self, host_template_id: u64) -> Self {
403 self.host_template_id = Some(host_template_id);
404 self
405 }
406
407 pub fn hosts(mut self, hosts: &ConfigObjectMap<Host>) -> Self {
413 if let Some(ref host_template) = self.host_template {
414 for host in hosts.values() {
415 if !host.has_template(host_template) {
416 panic!(
417 "Host '{}' does not have the template '{}'",
418 host.name,
419 host_template.name()
420 );
421 }
422 }
423 } else {
424 panic!("host_template must be set before hosts");
425 }
426
427 self.hosts = Some(hosts.into());
428 self
429 }
430
431 pub fn monitoring_cluster(mut self, monitoring_cluster: MonitoringCluster) -> Self {
437 self.monitoring_cluster = Some(MonitoringClusterRef::from(monitoring_cluster));
438 self
439 }
440
441 pub fn quorum_pct(mut self, quorum_pct: &str) -> Self {
447 self.quorum_pct = Some(quorum_pct.to_string());
448 self
449 }
450}
451
452#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
455pub struct BSMComponentRef {
456 name: String,
457 #[serde(
458 rename = "ref",
459 skip_serializing_if = "Option::is_none",
460 deserialize_with = "deserialize_readonly",
461 default
462 )]
463 ref_: Option<String>,
464}
465
466impl CreateFromJson for BSMComponentRef {}
469
470impl ConfigRef for BSMComponentRef {
471 type FullObject = BSMComponent;
472
473 fn ref_(&self) -> Option<String> {
475 self.ref_.clone()
476 }
477
478 fn name(&self) -> String {
480 self.name.clone()
481 }
482
483 fn unique_name(&self) -> String {
495 let name = self.name.clone();
496 match self.ref_.as_ref() {
497 Some(ref_) => ref_.clone(),
498 _ => name,
499 }
500 }
501}
502
503impl From<BSMComponent> for BSMComponentRef {
504 fn from(component: BSMComponent) -> Self {
505 Self {
506 name: component.name.clone(),
507 ref_: component.ref_.clone(),
508 }
509 }
510}
511
512impl From<Arc<BSMComponent>> for BSMComponentRef {
513 fn from(item: Arc<BSMComponent>) -> Self {
514 let component: BSMComponent = Arc::try_unwrap(item).unwrap_or_else(|arc| (*arc).clone());
515 BSMComponentRef::from(component)
516 }
517}
518
519impl From<&ConfigObjectMap<BSMComponent>> for ConfigRefMap<BSMComponentRef> {
520 fn from(components: &ConfigObjectMap<BSMComponent>) -> Self {
521 ref_map_from(components)
522 }
523}
524
525lazy_static! {
526 static ref QUORUM_PCT_REGEX: Regex = regex::Regex::new(r"^\d{1,3}\.\d{2}$").unwrap();
527}
528
529fn validated_pct_and_ratio(
537 percentage: &str,
538 number_of_hosts: usize,
539) -> Result<String, OpsviewConfigError> {
540 if percentage == "0.00" {
541 return Ok(percentage.to_string());
542 }
543
544 if percentage == "100.00" {
545 return Ok(percentage.to_string());
546 }
547
548 if number_of_hosts == 0 {
549 return Err(OpsviewConfigError::InvalidQuorum(
550 "The number of hosts must be greater than 0".to_string(),
551 ));
552 }
553
554 if !QUORUM_PCT_REGEX.is_match(percentage) {
556 return Err(OpsviewConfigError::InvalidQuorum(
557 "Must be a number with exactly 2 decimals".to_string(),
558 ));
559 }
560
561 let mut valid_percentages = Vec::new();
563 for host_count in 0..=number_of_hosts {
564 let pct = 100.0 * host_count as f64 / number_of_hosts as f64;
565 valid_percentages.push(format!("{:.2}", pct));
566 }
567
568 if valid_percentages.contains(&percentage.to_string()) {
570 Ok(percentage.to_string())
571 } else {
572 Err(OpsviewConfigError::InvalidQuorum(format!(
573 "The percentage '{}' is not a valid ratio for '{}' hosts",
574 percentage, number_of_hosts
575 )))
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582 use crate::config::{HostGroup, MonitoringCluster};
583 use pretty_assertions::assert_eq;
584
585 #[test]
586 fn test_is_valid_pct_and_ratio() {
587 assert!(validated_pct_and_ratio("100.00", 1).is_ok());
588 assert!(validated_pct_and_ratio("99.99", 10000).is_ok());
589 assert!(validated_pct_and_ratio("0.00", 0).is_ok());
590 assert!(validated_pct_and_ratio("0.01", 10000).is_ok());
591 assert!(validated_pct_and_ratio("0.99", 10000).is_ok());
592 assert!(validated_pct_and_ratio("1.00", 100).is_ok());
593 assert!(validated_pct_and_ratio("1.01", 10000).is_ok());
594 assert!(validated_pct_and_ratio("99.99", 10000).is_ok());
595 assert!(validated_pct_and_ratio("100.00", 1).is_ok());
596 assert!(validated_pct_and_ratio("50.00", 0).is_err());
597 assert!(validated_pct_and_ratio("100.01", 10000).is_err());
598 assert!(validated_pct_and_ratio("999.99", 1000).is_err());
599 assert!(validated_pct_and_ratio("999.99", 10).is_err());
600 assert!(validated_pct_and_ratio("999.99", 100).is_err());
601 assert!(validated_pct_and_ratio("100", 1).is_err());
602 assert!(validated_pct_and_ratio("1", 100).is_err());
603 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());
608 assert!(validated_pct_and_ratio("0.00", 2).is_ok());
609 assert!(validated_pct_and_ratio("50.00", 2).is_ok());
610 assert!(validated_pct_and_ratio("100.00", 2).is_ok());
611 assert!(validated_pct_and_ratio("90.00", 2).is_err());
612
613 let host_template = HostTemplate::builder()
614 .name("Host Template ")
615 .build()
616 .unwrap();
617
618 let mut host_templates = ConfigObjectMap::<HostTemplate>::new();
619 host_templates.add(host_template.clone());
620
621 let host_templates = host_templates;
622
623 let root_hostgroup = HostGroup::builder()
624 .name("Opsview")
625 .clear_parent()
626 .build()
627 .unwrap();
628
629 let cluster = MonitoringCluster::minimal("Cluster 1")
630 .expect("Failed to create cluster with name 'Cluster 1'");
631
632 let host = Host::builder()
633 .name("Host_1")
634 .alias("Host 1")
635 .ip("127.0.0.1")
636 .hostgroup(root_hostgroup)
637 .monitored_by(cluster)
638 .hosttemplates(&host_templates)
639 .build()
640 .unwrap();
641
642 let mut hosts = ConfigObjectMap::<Host>::new();
643 hosts.add(host);
644
645 let bsm_comp_1 = BSMComponent::builder()
646 .name("Comp 1")
647 .host_template(host_template.clone())
648 .hosts(&hosts)
649 .quorum_pct("100.00")
650 .build();
651
652 assert!(bsm_comp_1.is_ok());
653
654 let bsm_comp_2 = BSMComponent::builder()
655 .name("Comp 1")
656 .host_template(host_template.clone())
657 .hosts(&hosts)
658 .quorum_pct("100")
659 .build();
660
661 assert!(bsm_comp_2.is_err());
662 assert_eq!(
663 bsm_comp_2.err().unwrap().to_string(),
664 "Invalid quorum: Must be a number with exactly 2 decimals",
665 );
666 let bsm_comp_3 = BSMComponent::builder()
667 .name("Comp 1")
668 .host_template(host_template)
669 .hosts(&hosts)
670 .quorum_pct("90.00")
671 .build();
672
673 assert!(bsm_comp_3.is_err());
674 assert_eq!(
675 bsm_comp_3.err().unwrap().to_string(),
676 "Invalid quorum: The percentage '90.00' is not a valid ratio for '1' hosts",
677 );
678 }
679
680 #[test]
681 fn test_default() {
682 let bsm_component = BSMComponent::default();
683
684 assert!(bsm_component.name.is_empty());
685 }
686
687 #[test]
688 fn test_minimal() {
689 let bsm_component = BSMComponent::minimal("My BSM Component");
690
691 assert_eq!(bsm_component.unwrap().name, "My BSM Component".to_string());
692 }
693
694 #[test]
695 fn test_is_valid_bsmcomponent_name() {
696 assert!(validate_and_trim_bsmcomponent_name("ValidComponent123").is_ok());
698 assert!(validate_and_trim_bsmcomponent_name("Valid_Component-With.Symbols!").is_ok());
699 assert!(validate_and_trim_bsmcomponent_name("A").is_ok());
700 assert!(validate_and_trim_bsmcomponent_name(
701 "A component name with spaces and symbols *&^%$#@!"
702 )
703 .is_ok());
704 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());
713 }
715}