9fa3026a08
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
412 lines
9.7 KiB
Rust
412 lines
9.7 KiB
Rust
//! RRD toolkit - create/manage/update proxmox RRD (v2) file
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use anyhow::{bail, Error};
|
|
use serde::{Serialize, Deserialize};
|
|
use serde_json::json;
|
|
|
|
use proxmox_router::RpcEnvironment;
|
|
use proxmox_router::cli::{run_cli_command, complete_file_name, CliCommand, CliCommandMap, CliEnvironment};
|
|
use proxmox_schema::{api, ApiStringFormat, ApiType, IntegerSchema, Schema, StringSchema};
|
|
|
|
use proxmox_sys::fs::CreateOptions;
|
|
|
|
use proxmox_rrd::rrd::{CF, DST, RRA, RRD};
|
|
|
|
pub const RRA_INDEX_SCHEMA: Schema = IntegerSchema::new(
|
|
"Index of the RRA.")
|
|
.minimum(0)
|
|
.schema();
|
|
|
|
pub const RRA_CONFIG_STRING_SCHEMA: Schema = StringSchema::new(
|
|
"RRA configuration")
|
|
.format(&ApiStringFormat::PropertyString(&RRAConfig::API_SCHEMA))
|
|
.schema();
|
|
|
|
#[api(
|
|
properties: {},
|
|
default_key: "cf",
|
|
)]
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
/// RRA configuration
|
|
pub struct RRAConfig {
|
|
/// Time resolution
|
|
pub r: u64,
|
|
pub cf: CF,
|
|
/// Number of data points
|
|
pub n: u64,
|
|
}
|
|
|
|
#[api(
|
|
input: {
|
|
properties: {
|
|
path: {
|
|
description: "The filename."
|
|
},
|
|
},
|
|
},
|
|
)]
|
|
/// Dump the RRD file in JSON format
|
|
pub fn dump_rrd(path: String) -> Result<(), Error> {
|
|
|
|
let rrd = RRD::load(&PathBuf::from(path), false)?;
|
|
serde_json::to_writer_pretty(std::io::stdout(), &rrd)?;
|
|
println!("");
|
|
Ok(())
|
|
}
|
|
|
|
#[api(
|
|
input: {
|
|
properties: {
|
|
path: {
|
|
description: "The filename."
|
|
},
|
|
},
|
|
},
|
|
)]
|
|
/// RRD file information
|
|
pub fn rrd_info(path: String) -> Result<(), Error> {
|
|
|
|
let rrd = RRD::load(&PathBuf::from(path), false)?;
|
|
|
|
println!("DST: {:?}", rrd.source.dst);
|
|
|
|
for (i, rra) in rrd.rra_list.iter().enumerate() {
|
|
// use RRAConfig property string format
|
|
println!("RRA[{}]: {:?},r={},n={}", i, rra.cf, rra.resolution, rra.data.len());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[api(
|
|
input: {
|
|
properties: {
|
|
path: {
|
|
description: "The filename."
|
|
},
|
|
time: {
|
|
description: "Update time.",
|
|
optional: true,
|
|
},
|
|
value: {
|
|
description: "Update value.",
|
|
},
|
|
},
|
|
},
|
|
)]
|
|
/// Update the RRD database
|
|
pub fn update_rrd(
|
|
path: String,
|
|
time: Option<u64>,
|
|
value: f64,
|
|
) -> Result<(), Error> {
|
|
|
|
let path = PathBuf::from(path);
|
|
|
|
let time = time.map(|v| v as f64)
|
|
.unwrap_or_else(proxmox_time::epoch_f64);
|
|
|
|
let mut rrd = RRD::load(&path, false)?;
|
|
rrd.update(time, value);
|
|
|
|
rrd.save(&path, CreateOptions::new(), false)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[api(
|
|
input: {
|
|
properties: {
|
|
path: {
|
|
description: "The filename."
|
|
},
|
|
cf: {
|
|
type: CF,
|
|
},
|
|
resolution: {
|
|
description: "Time resulution",
|
|
},
|
|
start: {
|
|
description: "Start time. If not sepecified, we simply extract 10 data points.",
|
|
optional: true,
|
|
},
|
|
end: {
|
|
description: "End time (Unix Epoch). Default is the last update time.",
|
|
optional: true,
|
|
},
|
|
},
|
|
},
|
|
)]
|
|
/// Fetch data from the RRD file
|
|
pub fn fetch_rrd(
|
|
path: String,
|
|
cf: CF,
|
|
resolution: u64,
|
|
start: Option<u64>,
|
|
end: Option<u64>,
|
|
) -> Result<(), Error> {
|
|
|
|
let rrd = RRD::load(&PathBuf::from(path), false)?;
|
|
|
|
let data = rrd.extract_data(cf, resolution, start, end)?;
|
|
|
|
println!("{}", serde_json::to_string_pretty(&data)?);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[api(
|
|
input: {
|
|
properties: {
|
|
path: {
|
|
description: "The filename."
|
|
},
|
|
"rra-index": {
|
|
schema: RRA_INDEX_SCHEMA,
|
|
},
|
|
},
|
|
},
|
|
)]
|
|
/// Return the Unix timestamp of the first time slot inside the
|
|
/// specified RRA (slot start time)
|
|
pub fn first_update_time(
|
|
path: String,
|
|
rra_index: usize,
|
|
) -> Result<(), Error> {
|
|
|
|
let rrd = RRD::load(&PathBuf::from(path), false)?;
|
|
|
|
if rra_index >= rrd.rra_list.len() {
|
|
bail!("rra-index is out of range");
|
|
}
|
|
let rra = &rrd.rra_list[rra_index];
|
|
let duration = (rra.data.len() as u64)*rra.resolution;
|
|
let first = rra.slot_start_time((rrd.source.last_update as u64).saturating_sub(duration));
|
|
|
|
println!("{}", first);
|
|
Ok(())
|
|
}
|
|
|
|
#[api(
|
|
input: {
|
|
properties: {
|
|
path: {
|
|
description: "The filename."
|
|
},
|
|
},
|
|
},
|
|
)]
|
|
/// Return the Unix timestamp of the last update
|
|
pub fn last_update_time(path: String) -> Result<(), Error> {
|
|
|
|
let rrd = RRD::load(&PathBuf::from(path), false)?;
|
|
|
|
println!("{}", rrd.source.last_update);
|
|
Ok(())
|
|
}
|
|
|
|
#[api(
|
|
input: {
|
|
properties: {
|
|
path: {
|
|
description: "The filename."
|
|
},
|
|
},
|
|
},
|
|
)]
|
|
/// Return the time and value from the last update
|
|
pub fn last_update(path: String) -> Result<(), Error> {
|
|
|
|
let rrd = RRD::load(&PathBuf::from(path), false)?;
|
|
|
|
let result = json!({
|
|
"time": rrd.source.last_update,
|
|
"value": rrd.source.last_value,
|
|
});
|
|
|
|
println!("{}", serde_json::to_string_pretty(&result)?);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[api(
|
|
input: {
|
|
properties: {
|
|
dst: {
|
|
type: DST,
|
|
},
|
|
path: {
|
|
description: "The filename to create."
|
|
},
|
|
rra: {
|
|
description: "Configuration of contained RRAs.",
|
|
type: Array,
|
|
items: {
|
|
schema: RRA_CONFIG_STRING_SCHEMA,
|
|
}
|
|
},
|
|
},
|
|
},
|
|
)]
|
|
/// Create a new RRD file
|
|
pub fn create_rrd(
|
|
dst: DST,
|
|
path: String,
|
|
rra: Vec<String>,
|
|
) -> Result<(), Error> {
|
|
|
|
let mut rra_list = Vec::new();
|
|
|
|
for item in rra.iter() {
|
|
let rra: RRAConfig = serde_json::from_value(
|
|
RRAConfig::API_SCHEMA.parse_property_string(item)?
|
|
)?;
|
|
println!("GOT {:?}", rra);
|
|
rra_list.push(RRA::new(rra.cf, rra.r, rra.n as usize));
|
|
}
|
|
|
|
let path = PathBuf::from(path);
|
|
|
|
let rrd = RRD::new(dst, rra_list);
|
|
|
|
rrd.save(&path, CreateOptions::new(), false)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[api(
|
|
input: {
|
|
properties: {
|
|
path: {
|
|
description: "The filename."
|
|
},
|
|
"rra-index": {
|
|
schema: RRA_INDEX_SCHEMA,
|
|
},
|
|
slots: {
|
|
description: "The number of slots you want to add or remove.",
|
|
type: i64,
|
|
},
|
|
},
|
|
},
|
|
)]
|
|
/// Resize. Change the number of data slots for the specified RRA.
|
|
pub fn resize_rrd(
|
|
path: String,
|
|
rra_index: usize,
|
|
slots: i64,
|
|
) -> Result<(), Error> {
|
|
|
|
let path = PathBuf::from(&path);
|
|
|
|
let mut rrd = RRD::load(&path, false)?;
|
|
|
|
if rra_index >= rrd.rra_list.len() {
|
|
bail!("rra-index is out of range");
|
|
}
|
|
|
|
let rra = &rrd.rra_list[rra_index];
|
|
|
|
let new_slots = (rra.data.len() as i64) + slots;
|
|
|
|
if new_slots < 1 {
|
|
bail!("numer of new slots is too small ('{}' < 1)", new_slots);
|
|
}
|
|
|
|
if new_slots > 1024*1024 {
|
|
bail!("numer of new slots is too big ('{}' > 1M)", new_slots);
|
|
}
|
|
|
|
let rra_end = rra.slot_end_time(rrd.source.last_update as u64);
|
|
let rra_start = rra_end - rra.resolution*(rra.data.len() as u64);
|
|
let (start, reso, data) = rra.extract_data(rra_start, rra_end, rrd.source.last_update);
|
|
|
|
let mut new_rra = RRA::new(rra.cf, rra.resolution, new_slots as usize);
|
|
new_rra.last_count = rra.last_count;
|
|
|
|
new_rra.insert_data(start, reso, data)?;
|
|
|
|
rrd.rra_list[rra_index] = new_rra;
|
|
|
|
rrd.save(&path, CreateOptions::new(), false)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn main() -> Result<(), Error> {
|
|
|
|
let uid = nix::unistd::Uid::current();
|
|
|
|
let username = match nix::unistd::User::from_uid(uid)? {
|
|
Some(user) => user.name,
|
|
None => bail!("unable to get user name"),
|
|
};
|
|
|
|
let cmd_def = CliCommandMap::new()
|
|
.insert(
|
|
"create",
|
|
CliCommand::new(&API_METHOD_CREATE_RRD)
|
|
.arg_param(&["path"])
|
|
.completion_cb("path", complete_file_name)
|
|
)
|
|
.insert(
|
|
"dump",
|
|
CliCommand::new(&API_METHOD_DUMP_RRD)
|
|
.arg_param(&["path"])
|
|
.completion_cb("path", complete_file_name)
|
|
)
|
|
.insert(
|
|
"fetch",
|
|
CliCommand::new(&API_METHOD_FETCH_RRD)
|
|
.arg_param(&["path"])
|
|
.completion_cb("path", complete_file_name)
|
|
)
|
|
.insert(
|
|
"first",
|
|
CliCommand::new(&API_METHOD_FIRST_UPDATE_TIME)
|
|
.arg_param(&["path"])
|
|
.completion_cb("path", complete_file_name)
|
|
)
|
|
.insert(
|
|
"info",
|
|
CliCommand::new(&API_METHOD_RRD_INFO)
|
|
.arg_param(&["path"])
|
|
.completion_cb("path", complete_file_name)
|
|
)
|
|
.insert(
|
|
"last",
|
|
CliCommand::new(&API_METHOD_LAST_UPDATE_TIME)
|
|
.arg_param(&["path"])
|
|
.completion_cb("path", complete_file_name)
|
|
)
|
|
.insert(
|
|
"lastupdate",
|
|
CliCommand::new(&API_METHOD_LAST_UPDATE)
|
|
.arg_param(&["path"])
|
|
.completion_cb("path", complete_file_name)
|
|
)
|
|
.insert(
|
|
"resize",
|
|
CliCommand::new(&API_METHOD_RESIZE_RRD)
|
|
.arg_param(&["path"])
|
|
.completion_cb("path", complete_file_name)
|
|
)
|
|
.insert(
|
|
"update",
|
|
CliCommand::new(&API_METHOD_UPDATE_RRD)
|
|
.arg_param(&["path"])
|
|
.completion_cb("path", complete_file_name)
|
|
)
|
|
;
|
|
|
|
let mut rpcenv = CliEnvironment::new();
|
|
rpcenv.set_auth_id(Some(format!("{}@pam", username)));
|
|
|
|
run_cli_command(cmd_def, rpcenv, None);
|
|
|
|
Ok(())
|
|
|
|
}
|