use crate::{
error::{return_code_to_result, InvalidArgument, RrdError, RrdResult},
util::{path_to_str, ArrayOfStrings, NullTerminatedArrayOfStrings},
ConsolidationFn, Timestamp, TimestampExt,
};
use log::debug;
use std::convert;
use std::{ffi::CString, path::Path, ptr::null, time::Duration};
#[allow(clippy::too_many_arguments)]
pub fn create<'a>(
filename: &Path,
start: Timestamp,
step: Duration,
no_overwrite: bool,
template: Option<&Path>,
sources: &[&Path],
data_sources: impl IntoIterator<Item = &'a DataSource>,
round_robin_archives: impl IntoIterator<Item = &'a Archive>,
) -> RrdResult<()> {
let sources = sources
.iter()
.map(|p| path_to_str(p).and_then(|s| CString::new(s).map_err(convert::Into::into)))
.collect::<Result<NullTerminatedArrayOfStrings, _>>()?;
let filename = CString::new(path_to_str(filename)?)?;
let template = match template {
None => None,
Some(p) => Some(CString::new(path_to_str(p)?)?),
};
let args = data_sources
.into_iter()
.map(DataSource::as_arg_string)
.chain(round_robin_archives.into_iter().map(Archive::as_arg_string))
.map(CString::new)
.collect::<Result<ArrayOfStrings, _>>()?;
let start = start.try_as_time_t()?;
debug!(
"Create: file={filename:?} start={} step={} no_overwrite={no_overwrite} template={template:?} sources={sources:?} args={args:?}",
start,
step.as_secs()
);
#[allow(clippy::useless_conversion)]
let step = step
.as_secs()
.try_into()
.map_err(|_| RrdError::InvalidArgument("step is too large for librrd".to_string()))?;
let argc = args.len().try_into().map_err(|_| {
RrdError::InvalidArgument("too many create arguments for librrd".to_string())
})?;
let rc = unsafe {
rrd_sys::rrd_create_r2(
filename.as_ptr(),
step,
start,
no_overwrite.into(),
sources.as_ptr(),
template.map_or_else(null, |s| s.as_ptr()),
argc,
args.as_ptr(),
)
};
return_code_to_result(rc)
}
pub struct DataSource {
arg: String,
}
impl DataSource {
#[must_use]
pub fn gauge(
name: &DataSourceName,
heartbeat: u32,
min: Option<f64>,
max: Option<f64>,
) -> Self {
Self {
arg: format!(
"DS:{}:GAUGE:{heartbeat}:{}:{}",
name.name,
min.map_or_else(|| "U".to_string(), |m| m.to_string()),
max.map_or_else(|| "U".to_string(), |m| m.to_string())
),
}
}
#[must_use]
pub fn counter(
name: &DataSourceName,
heartbeat: u32,
min: Option<u64>,
max: Option<u64>,
) -> Self {
Self {
arg: format!(
"DS:{}:COUNTER:{heartbeat}:{}:{}",
name.name,
min.map_or_else(|| "U".to_string(), |m| m.to_string()),
max.map_or_else(|| "U".to_string(), |m| m.to_string())
),
}
}
#[must_use]
pub fn dcounter(
name: &DataSourceName,
heartbeat: u32,
min: Option<f64>,
max: Option<f64>,
) -> Self {
Self {
arg: format!(
"DS:{}:DCOUNTER:{heartbeat}:{}:{}",
name.name,
min.map_or_else(|| "U".to_string(), |m| m.to_string()),
max.map_or_else(|| "U".to_string(), |m| m.to_string())
),
}
}
#[must_use]
pub fn derive(
name: &DataSourceName,
heartbeat: u32,
min: Option<u64>,
max: Option<u64>,
) -> Self {
Self {
arg: format!(
"DS:{}:DERIVE:{heartbeat}:{}:{}",
name.name,
min.map_or_else(|| "U".to_string(), |m| m.to_string()),
max.map_or_else(|| "U".to_string(), |m| m.to_string())
),
}
}
#[must_use]
pub fn dderive(
name: &DataSourceName,
heartbeat: u32,
min: Option<f64>,
max: Option<f64>,
) -> Self {
Self {
arg: format!(
"DS:{}:DDERIVE:{heartbeat}:{}:{}",
name.name,
min.map_or_else(|| "U".to_string(), |m| m.to_string()),
max.map_or_else(|| "U".to_string(), |m| m.to_string())
),
}
}
#[must_use]
pub fn absolute(
name: &DataSourceName,
heartbeat: u32,
min: Option<u64>,
max: Option<u64>,
) -> Self {
Self {
arg: format!(
"DS:{}:ABSOLUTE:{heartbeat}:{}:{}",
name.name,
min.map_or_else(|| "U".to_string(), |m| m.to_string()),
max.map_or_else(|| "U".to_string(), |m| m.to_string())
),
}
}
#[must_use]
pub fn compute(name: &DataSourceName, rpn: &str) -> Self {
Self {
arg: format!("DS:{}:COMPUTE:{rpn}", name.name),
}
}
fn as_arg_string(&self) -> String {
self.arg.clone()
}
}
pub struct DataSourceName {
name: String,
}
impl DataSourceName {
pub fn new(name: impl Into<String>) -> Result<Self, InvalidArgument> {
let name = name.into();
validate_ds_name(&name)?;
Ok(Self { name })
}
pub fn mapped(
name: &str,
src_ds_name: &str,
index: Option<u32>,
) -> Result<Self, InvalidArgument> {
validate_mapped_ds_name(name)?;
validate_mapped_ds_name(src_ds_name)?;
Ok(Self {
name: match index {
None => format!("{name}={src_ds_name}"),
Some(i) => format!("{name}={src_ds_name}[{i}]"),
},
})
}
}
fn validate_ds_name(name: &str) -> Result<(), InvalidArgument> {
if !name.is_empty() && !name.contains(':') {
Ok(())
} else {
Err(InvalidArgument(
"Data source name must be non-empty and not contain ':'",
))
}
}
fn validate_mapped_ds_name(name: &str) -> Result<(), InvalidArgument> {
validate_ds_name(name)?;
if name.contains(['=', '[', ']']) {
Err(InvalidArgument(
"Mapped data source names must not contain '=', '[' or ']'",
))
} else {
Ok(())
}
}
pub struct Archive {
consolidation_fn: ConsolidationFn,
xfiles_factor: f64,
steps: u32,
rows: u32,
}
impl Archive {
pub fn new(
consolidation_fn: ConsolidationFn,
xfiles_factor: f64,
steps: u32,
rows: u32,
) -> Result<Self, InvalidArgument> {
if (0.0_f64..1.0_f64).contains(&xfiles_factor) {
Ok(Self {
consolidation_fn,
xfiles_factor,
steps,
rows,
})
} else {
Err(InvalidArgument("xfiles_factor must be in [0, 1]"))
}
}
}
impl Archive {
fn as_arg_string(&self) -> String {
format!(
"RRA:{}:{}:{}:{}",
self.consolidation_fn.as_arg_str(),
self.xfiles_factor,
self.steps,
self.rows
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ds_name_valid() {
assert!(DataSourceName::new("foo_123").is_ok());
assert!(DataSourceName::new("a1234567890123456789").is_ok());
assert!(DataSourceName::new("foo-bar").is_ok());
}
#[test]
fn ds_name_invalid() {
assert!(DataSourceName::new("").is_err());
assert!(DataSourceName::new("foo:bar").is_err());
}
#[test]
fn mapped_ds_name_validates_parts() {
assert_eq!(
"dst=src[2]",
DataSourceName::mapped("dst", "src", Some(2)).unwrap().name
);
assert!(DataSourceName::mapped("dst-name", "src", None).is_ok());
assert!(DataSourceName::mapped("dst", "src-name", None).is_ok());
assert!(DataSourceName::mapped("dst=name", "src", None).is_err());
assert!(DataSourceName::mapped("dst[1]", "src", None).is_err());
}
}