add simple rrd implementation
This commit is contained in:
		@ -21,3 +21,5 @@ pub mod client;
 | 
				
			|||||||
pub mod auth_helpers;
 | 
					pub mod auth_helpers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub mod auth;
 | 
					pub mod auth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod rrd;
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										82
									
								
								src/rrd/cache.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/rrd/cache.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,82 @@
 | 
				
			|||||||
 | 
					use std::time::{SystemTime, UNIX_EPOCH};
 | 
				
			||||||
 | 
					use std::path::PathBuf;
 | 
				
			||||||
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					use std::sync::{RwLock};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use anyhow::{format_err, Error};
 | 
				
			||||||
 | 
					use lazy_static::lazy_static;
 | 
				
			||||||
 | 
					use serde_json::Value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use proxmox::tools::fs::{create_path, CreateOptions};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const PBS_RRD_BASEDIR: &str = "/var/lib/proxmox-backup/rrdb";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					lazy_static!{
 | 
				
			||||||
 | 
					    static ref RRD_CACHE: RwLock<HashMap<String, RRD>> = {
 | 
				
			||||||
 | 
					        RwLock::new(HashMap::new())
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create rrdd stat dir with correct permission
 | 
				
			||||||
 | 
					pub fn create_rrdb_dir() -> Result<(), Error> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let backup_user = crate::backup::backup_user()?;
 | 
				
			||||||
 | 
					    let opts = CreateOptions::new()
 | 
				
			||||||
 | 
					        .owner(backup_user.uid)
 | 
				
			||||||
 | 
					        .group(backup_user.gid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    create_path(PBS_RRD_BASEDIR, None, Some(opts))
 | 
				
			||||||
 | 
					        .map_err(|err: Error| format_err!("unable to create rrdb stat dir - {}", err))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn now() -> Result<u64, Error> {
 | 
				
			||||||
 | 
					    let epoch = SystemTime::now().duration_since(UNIX_EPOCH)?;
 | 
				
			||||||
 | 
					    Ok(epoch.as_secs())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn update_value(rel_path: &str, value: f64) -> Result<(), Error> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut path = PathBuf::from(PBS_RRD_BASEDIR);
 | 
				
			||||||
 | 
					    path.push(rel_path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    std::fs::create_dir_all(path.parent().unwrap())?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut map = RRD_CACHE.write().unwrap();
 | 
				
			||||||
 | 
					    let now = now()?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if let Some(rrd) = map.get_mut(rel_path) {
 | 
				
			||||||
 | 
					        rrd.update(now, value);
 | 
				
			||||||
 | 
					        rrd.save(&path)?;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        let mut rrd = match RRD::load(&path) {
 | 
				
			||||||
 | 
					            Ok(rrd) => rrd,
 | 
				
			||||||
 | 
					            Err(_) => RRD::new(),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        rrd.update(now, value);
 | 
				
			||||||
 | 
					        rrd.save(&path)?;
 | 
				
			||||||
 | 
					        map.insert(rel_path.into(), rrd);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					   
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn extract_data(
 | 
				
			||||||
 | 
					    rel_path: &str,
 | 
				
			||||||
 | 
					    timeframe: RRDTimeFrameResolution,
 | 
				
			||||||
 | 
					    mode: RRDMode,
 | 
				
			||||||
 | 
					) -> Result<Value, Error> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let now = now()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let map = RRD_CACHE.read().unwrap();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if let Some(rrd) = map.get(rel_path) {
 | 
				
			||||||
 | 
					        Ok(rrd.extract_data(now, timeframe, mode))
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        Ok(RRD::new().extract_data(now, timeframe, mode))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								src/rrd/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/rrd/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					mod rrd;
 | 
				
			||||||
 | 
					pub use rrd::*;
 | 
				
			||||||
 | 
					mod cache;
 | 
				
			||||||
 | 
					pub use cache::*;
 | 
				
			||||||
							
								
								
									
										224
									
								
								src/rrd/rrd.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										224
									
								
								src/rrd/rrd.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,224 @@
 | 
				
			|||||||
 | 
					use std::io::Read;
 | 
				
			||||||
 | 
					use std::path::Path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use anyhow::{bail, Error};
 | 
				
			||||||
 | 
					use serde_json::{json, Value};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const RRD_DATA_ENTRIES: usize = 70;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Copy, Clone)]
 | 
				
			||||||
 | 
					pub enum RRDMode {
 | 
				
			||||||
 | 
					    Max,
 | 
				
			||||||
 | 
					    Average,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[repr(u64)]
 | 
				
			||||||
 | 
					#[derive(Copy, Clone)]
 | 
				
			||||||
 | 
					pub enum RRDTimeFrameResolution {
 | 
				
			||||||
 | 
					    Hour = 60,       // 1 min => last 70 minutes
 | 
				
			||||||
 | 
					    Day = 60*30,     // 30 min => last 35 hours
 | 
				
			||||||
 | 
					    Week = 60*180,   // 3 hours => about 8 days
 | 
				
			||||||
 | 
					    Month = 60*720,  // 12 hours => last 35 days
 | 
				
			||||||
 | 
					    Year = 60*10080, // 1 week => last 490 days
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[repr(C)]
 | 
				
			||||||
 | 
					#[derive(Default, Copy, Clone)]
 | 
				
			||||||
 | 
					struct RRDEntry {
 | 
				
			||||||
 | 
					    max: f64,
 | 
				
			||||||
 | 
					    average: f64,
 | 
				
			||||||
 | 
					    count: u64,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[repr(C)]
 | 
				
			||||||
 | 
					// Note: Avoid alignment problems by using 8byte types only
 | 
				
			||||||
 | 
					pub struct RRD {
 | 
				
			||||||
 | 
					    last_update: u64,
 | 
				
			||||||
 | 
					    hour: [RRDEntry; RRD_DATA_ENTRIES],
 | 
				
			||||||
 | 
					    day: [RRDEntry; RRD_DATA_ENTRIES],
 | 
				
			||||||
 | 
					    week: [RRDEntry; RRD_DATA_ENTRIES],
 | 
				
			||||||
 | 
					    month: [RRDEntry; RRD_DATA_ENTRIES],
 | 
				
			||||||
 | 
					    year: [RRDEntry; RRD_DATA_ENTRIES],
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl RRD {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn new() -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            last_update: 0,
 | 
				
			||||||
 | 
					            hour: [RRDEntry::default(); RRD_DATA_ENTRIES],
 | 
				
			||||||
 | 
					            day: [RRDEntry::default(); RRD_DATA_ENTRIES],
 | 
				
			||||||
 | 
					            week: [RRDEntry::default(); RRD_DATA_ENTRIES],
 | 
				
			||||||
 | 
					            month: [RRDEntry::default(); RRD_DATA_ENTRIES],
 | 
				
			||||||
 | 
					            year: [RRDEntry::default(); RRD_DATA_ENTRIES],
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn extract_data(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        epoch: u64,
 | 
				
			||||||
 | 
					        timeframe: RRDTimeFrameResolution,
 | 
				
			||||||
 | 
					        mode: RRDMode,
 | 
				
			||||||
 | 
					    ) -> Value {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let reso = timeframe as u64;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let end = reso*(epoch/reso);
 | 
				
			||||||
 | 
					        let start = end - reso*(RRD_DATA_ENTRIES as u64);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let rrd_end = reso*(self.last_update/reso);
 | 
				
			||||||
 | 
					        let rrd_start = rrd_end - reso*(RRD_DATA_ENTRIES as u64);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut list = Vec::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let data = match timeframe {
 | 
				
			||||||
 | 
					            RRDTimeFrameResolution::Hour => &self.hour,
 | 
				
			||||||
 | 
					            RRDTimeFrameResolution::Day => &self.day,
 | 
				
			||||||
 | 
					            RRDTimeFrameResolution::Week => &self.week,
 | 
				
			||||||
 | 
					            RRDTimeFrameResolution::Month => &self.month,
 | 
				
			||||||
 | 
					            RRDTimeFrameResolution::Year => &self.year,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        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(json!({ "time": t }));
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                let entry = data[index];
 | 
				
			||||||
 | 
					                if entry.count == 0 {
 | 
				
			||||||
 | 
					                    list.push(json!({ "time": t }));
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    let value = match mode {
 | 
				
			||||||
 | 
					                        RRDMode::Max => entry.max,
 | 
				
			||||||
 | 
					                        RRDMode::Average => entry.average,
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                    list.push(json!({ "time": t, "value": value }));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            t += reso; index = (index + 1) % RRD_DATA_ENTRIES;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        list.into()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn from_raw(mut raw: &[u8]) -> Result<Self, Error> {
 | 
				
			||||||
 | 
					        let expected_len = std::mem::size_of::<RRD>();
 | 
				
			||||||
 | 
					        if raw.len() != expected_len {
 | 
				
			||||||
 | 
					            bail!("RRD::from_raw failed - wrong data size ({} != {})", raw.len(), expected_len);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        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)?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(rrd)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn load(filename: &Path) -> Result<Self, Error> {
 | 
				
			||||||
 | 
					        let raw = proxmox::tools::fs::file_get_contents(filename)?;
 | 
				
			||||||
 | 
					        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::<RRD>())
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    fn compute_new_value(
 | 
				
			||||||
 | 
					        data: &[RRDEntry; RRD_DATA_ENTRIES],
 | 
				
			||||||
 | 
					        index: usize,
 | 
				
			||||||
 | 
					        value: f64,
 | 
				
			||||||
 | 
					    ) -> RRDEntry {        
 | 
				
			||||||
 | 
					        let RRDEntry { max, average, count } = data[index];
 | 
				
			||||||
 | 
					        let new_count = count + 1; // fixme: check overflow?
 | 
				
			||||||
 | 
					        if count == 0 {
 | 
				
			||||||
 | 
					            RRDEntry { max: value, average: value,  count: 1 }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            let new_max = if max > value { max } else { value };
 | 
				
			||||||
 | 
					            let new_average = (average*(count as f64) + value)/(new_count as f64);
 | 
				
			||||||
 | 
					            RRDEntry { max: new_max, average: new_average, count: new_count }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    pub fn update(&mut self, epoch: u64, value: f64) {
 | 
				
			||||||
 | 
					        // fixme: check time progress (epoch  last)
 | 
				
			||||||
 | 
					        let last = self.last_update;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let reso = RRDTimeFrameResolution::Hour as u64;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let min_time = epoch - (RRD_DATA_ENTRIES as u64)*reso;
 | 
				
			||||||
 | 
					        let mut t = last;
 | 
				
			||||||
 | 
					        let mut index = ((t/reso) % (RRD_DATA_ENTRIES as u64)) as usize;
 | 
				
			||||||
 | 
					        for _ in 0..RRD_DATA_ENTRIES {
 | 
				
			||||||
 | 
					            if t < min_time { self.hour[index] = RRDEntry::default(); }
 | 
				
			||||||
 | 
					            t += reso; index = (index + 1) % RRD_DATA_ENTRIES;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let index = ((epoch/reso) % (RRD_DATA_ENTRIES as u64)) as usize;
 | 
				
			||||||
 | 
					        self.hour[index] = Self::compute_new_value(&self.hour, index, value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let reso = RRDTimeFrameResolution::Day as u64; 
 | 
				
			||||||
 | 
					        let min_time = epoch - (RRD_DATA_ENTRIES as u64)*reso;
 | 
				
			||||||
 | 
					        let mut t = last;
 | 
				
			||||||
 | 
					        let mut index = ((t/reso) % (RRD_DATA_ENTRIES as u64)) as usize;
 | 
				
			||||||
 | 
					        for _ in 0..RRD_DATA_ENTRIES {
 | 
				
			||||||
 | 
					            if t < min_time { self.day[index] = RRDEntry::default(); }
 | 
				
			||||||
 | 
					            t += reso; index = (index + 1) % RRD_DATA_ENTRIES;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let index = ((epoch/reso) % (RRD_DATA_ENTRIES as u64)) as usize;
 | 
				
			||||||
 | 
					        self.day[index] = Self::compute_new_value(&self.day, index, value);
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					        let reso = RRDTimeFrameResolution::Week as u64; 
 | 
				
			||||||
 | 
					        let min_time = epoch - (RRD_DATA_ENTRIES as u64)*reso;
 | 
				
			||||||
 | 
					        let mut t = last;
 | 
				
			||||||
 | 
					        let mut index = ((t/reso) % (RRD_DATA_ENTRIES as u64)) as usize;
 | 
				
			||||||
 | 
					        for _ in 0..RRD_DATA_ENTRIES {
 | 
				
			||||||
 | 
					            if t < min_time { self.week[index] = RRDEntry::default(); }
 | 
				
			||||||
 | 
					            t += reso; index = (index + 1) % RRD_DATA_ENTRIES;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let index = ((epoch/reso) % (RRD_DATA_ENTRIES as u64)) as usize;
 | 
				
			||||||
 | 
					        self.week[index] = Self::compute_new_value(&self.week, index, value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let reso = RRDTimeFrameResolution::Month as u64; 
 | 
				
			||||||
 | 
					        let min_time = epoch - (RRD_DATA_ENTRIES as u64)*reso;
 | 
				
			||||||
 | 
					        let mut t = last;
 | 
				
			||||||
 | 
					        let mut index = ((t/reso) % (RRD_DATA_ENTRIES as u64)) as usize;
 | 
				
			||||||
 | 
					        for _ in 0..RRD_DATA_ENTRIES {
 | 
				
			||||||
 | 
					            if t < min_time { self.month[index] = RRDEntry::default(); }
 | 
				
			||||||
 | 
					            t += reso; index = (index + 1) % RRD_DATA_ENTRIES;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let index = ((epoch/reso) % (RRD_DATA_ENTRIES as u64)) as usize;
 | 
				
			||||||
 | 
					        self.month[index] = Self::compute_new_value(&self.month, index, value);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        let reso = RRDTimeFrameResolution::Year as u64; 
 | 
				
			||||||
 | 
					        let min_time = epoch - (RRD_DATA_ENTRIES as u64)*reso;
 | 
				
			||||||
 | 
					        let mut t = last;
 | 
				
			||||||
 | 
					        let mut index = ((t/reso) % (RRD_DATA_ENTRIES as u64)) as usize;
 | 
				
			||||||
 | 
					        for _ in 0..RRD_DATA_ENTRIES {
 | 
				
			||||||
 | 
					            if t < min_time { self.year[index] = RRDEntry::default(); }
 | 
				
			||||||
 | 
					            t += reso; index = (index + 1) % RRD_DATA_ENTRIES;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let index = ((epoch/reso) % (RRD_DATA_ENTRIES as u64)) as usize;
 | 
				
			||||||
 | 
					        self.year[index] = Self::compute_new_value(&self.year, index, value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.last_update = epoch;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user