use std::io::Read; use std::path::Path; use anyhow::Error; use crate::api2::types::{RRDMode, RRDTimeFrameResolution}; pub const RRD_DATA_ENTRIES: usize = 70; // openssl::sha::sha256(b"Proxmox Round Robin Database file v1.0")[0..8]; pub const PROXMOX_RRD_MAGIC_1_0: [u8; 8] = [206, 46, 26, 212, 172, 158, 5, 186]; use bitflags::bitflags; bitflags!{ pub struct RRAFlags: u64 { // Data Source Types const DST_GAUGE = 1; const DST_DERIVE = 2; const DST_COUNTER = 4; const DST_MASK = 255; // first 8 bits // Consolidation Functions const CF_AVERAGE = 1 << 8; const CF_MAX = 2 << 8; const CF_MASK = 255 << 8; } } pub enum DST { Gauge, Derive, } #[repr(C)] struct RRA { flags: RRAFlags, resolution: u64, last_update: f64, last_count: u64, counter_value: f64, // used for derive/counters data: [f64; RRD_DATA_ENTRIES], } impl RRA { fn new(flags: RRAFlags, resolution: u64) -> Self { Self { flags, resolution, last_update: 0.0, last_count: 0, counter_value: f64::NAN, data: [f64::NAN; RRD_DATA_ENTRIES], } } fn delete_old(&mut self, time: f64) { let epoch = time as u64; let last_update = self.last_update as u64; let reso = self.resolution; let min_time = epoch - (RRD_DATA_ENTRIES as u64)*reso; let min_time = (min_time/reso + 1)*reso; let mut t = last_update.saturating_sub((RRD_DATA_ENTRIES as u64)*reso); let mut index = ((t/reso) % (RRD_DATA_ENTRIES as u64)) as usize; for _ in 0..RRD_DATA_ENTRIES { t += reso; index = (index + 1) % RRD_DATA_ENTRIES; if t < min_time { self.data[index] = f64::NAN; } else { break; } } } fn compute_new_value(&mut self, time: f64, value: f64) { let epoch = time as u64; let last_update = self.last_update as u64; let reso = self.resolution; let index = ((epoch/reso) % (RRD_DATA_ENTRIES as u64)) as usize; let last_index = ((last_update/reso) % (RRD_DATA_ENTRIES as u64)) as usize; if (epoch - (last_update as u64)) > reso || index != last_index { self.last_count = 0; } let last_value = self.data[index]; if last_value.is_nan() { self.last_count = 0; } let new_count = if self.last_count < u64::MAX { self.last_count + 1 } else { u64::MAX // should never happen }; if self.last_count == 0 { self.data[index] = value; self.last_count = 1; } else { let new_value = if self.flags.contains(RRAFlags::CF_MAX) { if last_value > value { last_value } else { value } } else if self.flags.contains(RRAFlags::CF_AVERAGE) { (last_value*(self.last_count as f64))/(new_count as f64) + value/(new_count as f64) } else { eprintln!("rrdb update failed - unknown CF"); return; }; self.data[index] = new_value; self.last_count = new_count; } self.last_update = time; } fn update(&mut self, time: f64, mut value: f64) { if time <= self.last_update { eprintln!("rrdb update failed - time in past ({} < {})", time, self.last_update); } if value.is_nan() { eprintln!("rrdb update failed - new value is NAN"); return; } // derive counter value if self.flags.intersects(RRAFlags::DST_DERIVE | RRAFlags::DST_COUNTER) { let time_diff = time - self.last_update; let diff = if self.counter_value.is_nan() { 0.0 } else { if self.flags.contains(RRAFlags::DST_COUNTER) { // check for overflow if value < 0.0 { eprintln!("rrdb update failed - got negative value for counter"); return; } // Note: We do not try automatic overflow corrections if value < self.counter_value { // overflow or counter reset self.counter_value = value; eprintln!("rrdb update failed - conter overflow/reset detected"); return; } else { value - self.counter_value } } else { value - self.counter_value } }; self.counter_value = value; value = diff/time_diff; } self.delete_old(time); self.compute_new_value(time, value); } } #[repr(C)] // Note: Avoid alignment problems by using 8byte types only pub struct RRD { magic: [u8; 8], hour_avg: RRA, hour_max: RRA, day_avg: RRA, day_max: RRA, week_avg: RRA, week_max: RRA, month_avg: RRA, month_max: RRA, year_avg: RRA, year_max: RRA, } impl RRD { pub fn new(dst: DST) -> Self { let flags = match dst { DST::Gauge => RRAFlags::DST_GAUGE, DST::Derive => RRAFlags::DST_DERIVE, }; Self { magic: PROXMOX_RRD_MAGIC_1_0, hour_avg: RRA::new( flags | RRAFlags::CF_AVERAGE, RRDTimeFrameResolution::Hour as u64, ), hour_max: RRA::new( flags | RRAFlags::CF_MAX, RRDTimeFrameResolution::Hour as u64, ), day_avg: RRA::new( flags | RRAFlags::CF_AVERAGE, RRDTimeFrameResolution::Day as u64, ), day_max: RRA::new( flags | RRAFlags::CF_MAX, RRDTimeFrameResolution::Day as u64, ), week_avg: RRA::new( flags | RRAFlags::CF_AVERAGE, RRDTimeFrameResolution::Week as u64, ), week_max: RRA::new( flags | RRAFlags::CF_MAX, RRDTimeFrameResolution::Week as u64, ), month_avg: RRA::new( flags | RRAFlags::CF_AVERAGE, RRDTimeFrameResolution::Month as u64, ), month_max: RRA::new( flags | RRAFlags::CF_MAX, RRDTimeFrameResolution::Month as u64, ), year_avg: RRA::new( flags | RRAFlags::CF_AVERAGE, RRDTimeFrameResolution::Year as u64, ), year_max: RRA::new( flags | RRAFlags::CF_MAX, RRDTimeFrameResolution::Year as u64, ), } } pub fn extract_data( &self, time: f64, timeframe: RRDTimeFrameResolution, mode: RRDMode, ) -> (u64, u64, Vec>) { let epoch = time as u64; let reso = timeframe as u64; let end = reso*(epoch/reso + 1); let start = end - reso*(RRD_DATA_ENTRIES as u64); let mut list = Vec::new(); let raa = match (mode, timeframe) { (RRDMode::Average, RRDTimeFrameResolution::Hour) => &self.hour_avg, (RRDMode::Max, RRDTimeFrameResolution::Hour) => &self.hour_max, (RRDMode::Average, RRDTimeFrameResolution::Day) => &self.day_avg, (RRDMode::Max, RRDTimeFrameResolution::Day) => &self.day_max, (RRDMode::Average, RRDTimeFrameResolution::Week) => &self.week_avg, (RRDMode::Max, RRDTimeFrameResolution::Week) => &self.week_max, (RRDMode::Average, RRDTimeFrameResolution::Month) => &self.month_avg, (RRDMode::Max, RRDTimeFrameResolution::Month) => &self.month_max, (RRDMode::Average, RRDTimeFrameResolution::Year) => &self.year_avg, (RRDMode::Max, RRDTimeFrameResolution::Year) => &self.year_max, }; let rrd_end = reso*((raa.last_update as u64)/reso); let rrd_start = rrd_end - reso*(RRD_DATA_ENTRIES as u64); let mut t = start; let mut index = ((t/reso) % (RRD_DATA_ENTRIES as u64)) as usize; for _ in 0..RRD_DATA_ENTRIES { if t < rrd_start || t > rrd_end { list.push(None); } else { let value = raa.data[index]; if value.is_nan() { list.push(None); } else { list.push(Some(value)); } } t += reso; index = (index + 1) % RRD_DATA_ENTRIES; } (start, reso, list.into()) } pub fn from_raw(mut raw: &[u8]) -> Result { let expected_len = std::mem::size_of::(); if raw.len() != expected_len { let msg = format!("wrong data size ({} != {})", raw.len(), expected_len); return Err(std::io::Error::new(std::io::ErrorKind::Other, msg)); } let mut rrd: RRD = unsafe { std::mem::zeroed() }; unsafe { let rrd_slice = std::slice::from_raw_parts_mut(&mut rrd as *mut _ as *mut u8, expected_len); raw.read_exact(rrd_slice)?; } if rrd.magic != PROXMOX_RRD_MAGIC_1_0 { let msg = format!("wrong magic number"); return Err(std::io::Error::new(std::io::ErrorKind::Other, msg)); } Ok(rrd) } pub fn load(path: &Path) -> Result { let raw = std::fs::read(path)?; Self::from_raw(&raw) } pub fn save(&self, filename: &Path) -> Result<(), Error> { use proxmox::tools::{fs::replace_file, fs::CreateOptions}; let rrd_slice = unsafe { std::slice::from_raw_parts(self as *const _ as *const u8, std::mem::size_of::()) }; let backup_user = crate::backup::backup_user()?; let mode = nix::sys::stat::Mode::from_bits_truncate(0o0644); // set the correct owner/group/permissions while saving file // owner(rw) = backup, group(r)= backup let options = CreateOptions::new() .perm(mode) .owner(backup_user.uid) .group(backup_user.gid); replace_file(filename, rrd_slice, options)?; Ok(()) } pub fn update(&mut self, time: f64, value: f64) { self.hour_avg.update(time, value); self.hour_max.update(time, value); self.day_avg.update(time, value); self.day_max.update(time, value); self.week_avg.update(time, value); self.week_max.update(time, value); self.month_avg.update(time, value); self.month_max.update(time, value); self.year_avg.update(time, value); self.year_max.update(time, value); } }