rrdcached_client/
create.rs

1use crate::{
2    consolidation_function::ConsolidationFunction,
3    errors::RRDCachedClientError,
4    sanitisation::{check_data_source_name, check_rrd_path},
5};
6
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub enum CreateDataSourceType {
9    Gauge,
10    Counter,
11    DCounter,
12    Derive,
13    DDerive,
14    Absolute,
15}
16
17impl CreateDataSourceType {
18    pub fn to_str(self) -> &'static str {
19        match self {
20            CreateDataSourceType::Gauge => "GAUGE",
21            CreateDataSourceType::Counter => "COUNTER",
22            CreateDataSourceType::DCounter => "DCOUNTER",
23            CreateDataSourceType::Derive => "DERIVE",
24            CreateDataSourceType::DDerive => "DDERIVE",
25            CreateDataSourceType::Absolute => "ABSOLUTE",
26        }
27    }
28}
29
30/// Arguments for a data source (DS).
31#[derive(Debug)]
32pub struct CreateDataSource {
33    /// Name of the data source.
34    /// Must be between 1 and 64 characters and only contain alphanumeric characters and underscores
35    /// and dashes.
36    pub name: String,
37
38    /// Minimum value
39    pub minimum: Option<f64>,
40
41    /// Maximum value
42    pub maximum: Option<f64>,
43
44    /// Heartbeat, if no data is received for this amount of time,
45    /// the value is unknown.
46    pub heartbeat: i64,
47
48    /// Type of the data source
49    pub serie_type: CreateDataSourceType,
50}
51
52impl CreateDataSource {
53    /// Check that the content is valid.
54    pub fn validate(&self) -> Result<(), RRDCachedClientError> {
55        if self.heartbeat <= 0 {
56            return Err(RRDCachedClientError::InvalidCreateDataSerie(
57                "heartbeat must be greater than 0".to_string(),
58            ));
59        }
60        if let Some(minimum) = self.minimum {
61            if let Some(maximum) = self.maximum {
62                if maximum <= minimum {
63                    return Err(RRDCachedClientError::InvalidCreateDataSerie(
64                        "maximum must be greater than to minimum".to_string(),
65                    ));
66                }
67            }
68        }
69
70        check_data_source_name(&self.name)?;
71
72        Ok(())
73    }
74
75    /// Convert to a string argument parameter.
76    pub fn to_str(&self) -> String {
77        format!(
78            "DS:{}:{}:{}:{}:{}",
79            self.name,
80            self.serie_type.to_str(),
81            self.heartbeat,
82            match self.minimum {
83                Some(minimum) => minimum.to_string(),
84                None => "U".to_string(),
85            },
86            match self.maximum {
87                Some(maximum) => maximum.to_string(),
88                None => "U".to_string(),
89            }
90        )
91    }
92}
93
94/// Arguments for a round robin archive (RRA).
95#[derive(Debug)]
96pub struct CreateRoundRobinArchive {
97    /// Archive types are AVERAGE, MIN, MAX, LAST.
98    pub consolidation_function: ConsolidationFunction,
99
100    /// Number between 0 and 1 to accept unknown data
101    /// 0.5 means that if more of 50% of the data points are unknown,
102    /// the value is unknown.
103    pub xfiles_factor: f64,
104
105    /// Number of steps that are used to calculate the value
106    pub steps: i64,
107
108    /// Number of rows in the archive
109    pub rows: i64,
110}
111
112impl CreateRoundRobinArchive {
113    /// Check that the content is valid.
114    pub fn validate(&self) -> Result<(), RRDCachedClientError> {
115        if self.xfiles_factor < 0.0 || self.xfiles_factor > 1.0 {
116            return Err(RRDCachedClientError::InvalidCreateDataSerie(
117                "xfiles_factor must be between 0 and 1".to_string(),
118            ));
119        }
120        if self.steps <= 0 {
121            return Err(RRDCachedClientError::InvalidCreateDataSerie(
122                "steps must be greater than 0".to_string(),
123            ));
124        }
125        if self.rows <= 0 {
126            return Err(RRDCachedClientError::InvalidCreateDataSerie(
127                "rows must be greater than 0".to_string(),
128            ));
129        }
130        Ok(())
131    }
132
133    /// Convert to a string argument parameter.
134    pub fn to_str(&self) -> String {
135        format!(
136            "RRA:{}:{}:{}:{}",
137            self.consolidation_function.to_str(),
138            self.xfiles_factor,
139            self.steps,
140            self.rows
141        )
142    }
143}
144
145/// Arguments to create a new RRD file
146#[derive(Debug)]
147pub struct CreateArguments {
148    /// Path to the RRD file
149    /// The path must be between 1 and 64 characters and only contain alphanumeric characters and underscores
150    ///
151    /// Does **not** end with .rrd
152    pub path: String,
153
154    /// List of data sources, the order is important
155    /// Must be at least one.
156    pub data_sources: Vec<CreateDataSource>,
157
158    /// List of round robin archives.
159    /// Must be at least one.
160    pub round_robin_archives: Vec<CreateRoundRobinArchive>,
161
162    /// Start time of the first data point
163    pub start_timestamp: u64,
164
165    /// Number of seconds between two data points
166    pub step_seconds: u64,
167}
168
169impl CreateArguments {
170    /// Check that the content is valid.
171    pub fn validate(&self) -> Result<(), RRDCachedClientError> {
172        if self.data_sources.is_empty() {
173            return Err(RRDCachedClientError::InvalidCreateDataSerie(
174                "at least one data serie is required".to_string(),
175            ));
176        }
177        if self.round_robin_archives.is_empty() {
178            return Err(RRDCachedClientError::InvalidCreateDataSerie(
179                "at least one round robin archive is required".to_string(),
180            ));
181        }
182        for data_serie in &self.data_sources {
183            data_serie.validate()?;
184        }
185        for rr_archive in &self.round_robin_archives {
186            rr_archive.validate()?;
187        }
188        check_rrd_path(&self.path)?;
189        Ok(())
190    }
191
192    /// Convert to a string argument parameter.
193    pub fn to_str(&self) -> String {
194        let mut result = format!(
195            "{}.rrd -s {} -b {}",
196            self.path, self.step_seconds, self.start_timestamp
197        );
198        for data_serie in &self.data_sources {
199            result.push(' ');
200            result.push_str(&data_serie.to_str());
201        }
202        for rr_archive in &self.round_robin_archives {
203            result.push(' ');
204            result.push_str(&rr_archive.to_str());
205        }
206        result
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    // Test for CreateDataSourceType to_str method
215    #[test]
216    fn test_create_data_source_type_to_str() {
217        assert_eq!(CreateDataSourceType::Gauge.to_str(), "GAUGE");
218        assert_eq!(CreateDataSourceType::Counter.to_str(), "COUNTER");
219        assert_eq!(CreateDataSourceType::DCounter.to_str(), "DCOUNTER");
220        assert_eq!(CreateDataSourceType::Derive.to_str(), "DERIVE");
221        assert_eq!(CreateDataSourceType::DDerive.to_str(), "DDERIVE");
222        assert_eq!(CreateDataSourceType::Absolute.to_str(), "ABSOLUTE");
223    }
224
225    // Test for CreateDataSource validate method
226    #[test]
227    fn test_create_data_source_validate() {
228        let valid_ds = CreateDataSource {
229            name: "valid_name_1".to_string(),
230            minimum: Some(0.0),
231            maximum: Some(100.0),
232            heartbeat: 300,
233            serie_type: CreateDataSourceType::Gauge,
234        };
235        assert!(valid_ds.validate().is_ok());
236
237        let invalid_ds_name = CreateDataSource {
238            name: "Invalid Name!".to_string(), // Invalid due to space and exclamation
239            ..valid_ds
240        };
241        assert!(invalid_ds_name.validate().is_err());
242
243        let invalid_ds_heartbeat = CreateDataSource {
244            heartbeat: -1, // Invalid heartbeat
245            name: "valid_name_2".to_string(),
246            ..valid_ds
247        };
248        assert!(invalid_ds_heartbeat.validate().is_err());
249
250        let invalid_ds_min_max = CreateDataSource {
251            minimum: Some(100.0),
252            maximum: Some(50.0), // Invalid minimum and maximum
253            name: "valid_name_3".to_string(),
254            ..valid_ds
255        };
256        assert!(invalid_ds_min_max.validate().is_err());
257
258        // Maximum below minimum
259        let invalid_ds_max = CreateDataSource {
260            minimum: Some(100.0),
261            maximum: Some(0.0),
262            name: "valid_name_5".to_string(),
263            ..valid_ds
264        };
265        assert!(invalid_ds_max.validate().is_err());
266
267        // Maximum but no minimum
268        let valid_ds_max = CreateDataSource {
269            maximum: Some(100.0),
270            name: "valid_name_6".to_string(),
271            ..valid_ds
272        };
273        assert!(valid_ds_max.validate().is_ok());
274
275        // Minimum but no maximum
276        let valid_ds_min = CreateDataSource {
277            minimum: Some(-100.0),
278            name: "valid_name_7".to_string(),
279            ..valid_ds
280        };
281        assert!(valid_ds_min.validate().is_ok());
282    }
283
284    // Test for CreateDataSource to_str method
285    #[test]
286    fn test_create_data_source_to_str() {
287        let ds = CreateDataSource {
288            name: "test_ds".to_string(),
289            minimum: Some(10.0),
290            maximum: Some(100.0),
291            heartbeat: 600,
292            serie_type: CreateDataSourceType::Gauge,
293        };
294        assert_eq!(ds.to_str(), "DS:test_ds:GAUGE:600:10:100");
295
296        let ds = CreateDataSource {
297            name: "test_ds".to_string(),
298            minimum: None,
299            maximum: None,
300            heartbeat: 600,
301            serie_type: CreateDataSourceType::Gauge,
302        };
303        assert_eq!(ds.to_str(), "DS:test_ds:GAUGE:600:U:U");
304    }
305
306    // Test for CreateRoundRobinArchive validate method
307    #[test]
308    fn test_create_round_robin_archive_validate() {
309        let valid_rra = CreateRoundRobinArchive {
310            consolidation_function: ConsolidationFunction::Average,
311            xfiles_factor: 0.5,
312            steps: 1,
313            rows: 100,
314        };
315        assert!(valid_rra.validate().is_ok());
316
317        let invalid_rra_xff = CreateRoundRobinArchive {
318            xfiles_factor: -0.1, // Invalid xfiles_factor
319            ..valid_rra
320        };
321        assert!(invalid_rra_xff.validate().is_err());
322
323        let invalid_rra_steps = CreateRoundRobinArchive {
324            steps: 0, // Invalid steps
325            ..valid_rra
326        };
327        assert!(invalid_rra_steps.validate().is_err());
328
329        let invalid_rra_rows = CreateRoundRobinArchive {
330            rows: -100, // Invalid rows
331            ..valid_rra
332        };
333        assert!(invalid_rra_rows.validate().is_err());
334    }
335
336    // Test for CreateRoundRobinArchive to_str method
337    #[test]
338    fn test_create_round_robin_archive_to_str() {
339        let rra = CreateRoundRobinArchive {
340            consolidation_function: ConsolidationFunction::Max,
341            xfiles_factor: 0.5,
342            steps: 1,
343            rows: 100,
344        };
345        assert_eq!(rra.to_str(), "RRA:MAX:0.5:1:100");
346    }
347
348    // Test for CreateArguments validate method
349    #[test]
350    fn test_create_arguments_validate() {
351        let valid_args = CreateArguments {
352            path: "valid_path".to_string(),
353            data_sources: vec![CreateDataSource {
354                name: "ds1".to_string(),
355                minimum: Some(0.0),
356                maximum: Some(100.0),
357                heartbeat: 300,
358                serie_type: CreateDataSourceType::Gauge,
359            }],
360            round_robin_archives: vec![CreateRoundRobinArchive {
361                consolidation_function: ConsolidationFunction::Average,
362                xfiles_factor: 0.5,
363                steps: 1,
364                rows: 100,
365            }],
366            start_timestamp: 1609459200,
367            step_seconds: 300,
368        };
369        assert!(valid_args.validate().is_ok());
370
371        let invalid_args_no_ds = CreateArguments {
372            data_sources: vec![],
373            path: "valid_path".to_string(),
374            ..valid_args
375        };
376        assert!(invalid_args_no_ds.validate().is_err());
377
378        let invalid_args_no_rra = CreateArguments {
379            round_robin_archives: vec![],
380            path: "valid_path".to_string(),
381            ..valid_args
382        };
383        assert!(invalid_args_no_rra.validate().is_err());
384    }
385
386    // Test for CreateArguments to_str method
387    #[test]
388    fn test_create_arguments_to_str() {
389        let args = CreateArguments {
390            path: "test_path".to_string(),
391            data_sources: vec![CreateDataSource {
392                name: "ds1".to_string(),
393                minimum: Some(0.0),
394                maximum: Some(100.0),
395                heartbeat: 300,
396                serie_type: CreateDataSourceType::Gauge,
397            }],
398            round_robin_archives: vec![CreateRoundRobinArchive {
399                consolidation_function: ConsolidationFunction::Average,
400                xfiles_factor: 0.5,
401                steps: 1,
402                rows: 100,
403            }],
404            start_timestamp: 1609459200,
405            step_seconds: 300,
406        };
407        let expected_str =
408            "test_path.rrd -s 300 -b 1609459200 DS:ds1:GAUGE:300:0:100 RRA:AVERAGE:0.5:1:100";
409        assert_eq!(args.to_str(), expected_str);
410    }
411}