]>
Commit | Line | Data |
---|---|---|
4cd28918 DM |
1 | //! RRD toolkit - create/manage/update proxmox RRD (v2) file |
2 | ||
3 | use std::path::PathBuf; | |
4 | ||
5 | use anyhow::{bail, Error}; | |
d5b9d1f4 | 6 | use serde::{Deserialize, Serialize}; |
392d646f | 7 | use serde_json::json; |
4cd28918 | 8 | |
d5b9d1f4 TL |
9 | use proxmox_router::cli::{ |
10 | complete_file_name, run_cli_command, CliCommand, CliCommandMap, CliEnvironment, | |
11 | }; | |
4cd28918 | 12 | use proxmox_router::RpcEnvironment; |
cb32acc7 | 13 | use proxmox_schema::{api, ApiStringFormat, ApiType, IntegerSchema, Schema, StringSchema}; |
4cd28918 | 14 | |
a092ef9c | 15 | use proxmox_sys::fs::CreateOptions; |
4cd28918 | 16 | |
2f942833 | 17 | use proxmox_rrd::rrd::{AggregationFn, Archive, DataSourceType, Database}; |
4cd28918 | 18 | |
d5b9d1f4 | 19 | pub const RRA_INDEX_SCHEMA: Schema = IntegerSchema::new("Index of the RRA.").minimum(0).schema(); |
392d646f | 20 | |
d5b9d1f4 | 21 | pub const RRA_CONFIG_STRING_SCHEMA: Schema = StringSchema::new("RRA configuration") |
4cd28918 DM |
22 | .format(&ApiStringFormat::PropertyString(&RRAConfig::API_SCHEMA)) |
23 | .schema(); | |
24 | ||
25 | #[api( | |
26 | properties: {}, | |
27 | default_key: "cf", | |
28 | )] | |
29 | #[derive(Debug, Serialize, Deserialize)] | |
30 | /// RRA configuration | |
31 | pub struct RRAConfig { | |
32 | /// Time resolution | |
33 | pub r: u64, | |
2f942833 | 34 | pub cf: AggregationFn, |
4cd28918 DM |
35 | /// Number of data points |
36 | pub n: u64, | |
37 | } | |
38 | ||
39 | #[api( | |
40 | input: { | |
41 | properties: { | |
42 | path: { | |
43 | description: "The filename." | |
44 | }, | |
45 | }, | |
46 | }, | |
47 | )] | |
392d646f DM |
48 | /// Dump the RRD file in JSON format |
49 | pub fn dump_rrd(path: String) -> Result<(), Error> { | |
2f942833 | 50 | let rrd = Database::load(&PathBuf::from(path), false)?; |
4cd28918 | 51 | serde_json::to_writer_pretty(std::io::stdout(), &rrd)?; |
d80d195c | 52 | println!(); |
392d646f DM |
53 | Ok(()) |
54 | } | |
55 | ||
56 | #[api( | |
57 | input: { | |
58 | properties: { | |
59 | path: { | |
60 | description: "The filename." | |
61 | }, | |
62 | }, | |
63 | }, | |
64 | )] | |
65 | /// RRD file information | |
66 | pub fn rrd_info(path: String) -> Result<(), Error> { | |
2f942833 | 67 | let rrd = Database::load(&PathBuf::from(path), false)?; |
392d646f DM |
68 | |
69 | println!("DST: {:?}", rrd.source.dst); | |
70 | ||
71 | for (i, rra) in rrd.rra_list.iter().enumerate() { | |
72 | // use RRAConfig property string format | |
d5b9d1f4 TL |
73 | println!( |
74 | "RRA[{}]: {:?},r={},n={}", | |
75 | i, | |
76 | rra.cf, | |
77 | rra.resolution, | |
78 | rra.data.len() | |
79 | ); | |
392d646f DM |
80 | } |
81 | ||
4cd28918 DM |
82 | Ok(()) |
83 | } | |
84 | ||
85 | #[api( | |
86 | input: { | |
87 | properties: { | |
88 | path: { | |
89 | description: "The filename." | |
90 | }, | |
91 | time: { | |
92 | description: "Update time.", | |
93 | optional: true, | |
94 | }, | |
95 | value: { | |
96 | description: "Update value.", | |
97 | }, | |
98 | }, | |
99 | }, | |
100 | )] | |
392d646f | 101 | /// Update the RRD database |
d5b9d1f4 | 102 | pub fn update_rrd(path: String, time: Option<u64>, value: f64) -> Result<(), Error> { |
4cd28918 DM |
103 | let path = PathBuf::from(path); |
104 | ||
d5b9d1f4 TL |
105 | let time = time |
106 | .map(|v| v as f64) | |
4cd28918 DM |
107 | .unwrap_or_else(proxmox_time::epoch_f64); |
108 | ||
2f942833 | 109 | let mut rrd = Database::load(&path, false)?; |
4cd28918 DM |
110 | rrd.update(time, value); |
111 | ||
a7ee3455 | 112 | rrd.save(&path, CreateOptions::new(), false)?; |
4cd28918 DM |
113 | |
114 | Ok(()) | |
115 | } | |
116 | ||
117 | #[api( | |
118 | input: { | |
119 | properties: { | |
120 | path: { | |
121 | description: "The filename." | |
122 | }, | |
123 | cf: { | |
724c3dda | 124 | type: AggregationFn, |
4cd28918 DM |
125 | }, |
126 | resolution: { | |
109902fb | 127 | description: "Time resolution", |
4cd28918 DM |
128 | }, |
129 | start: { | |
109902fb | 130 | description: "Start time. If not specified, we simply extract 10 data points.", |
4cd28918 DM |
131 | optional: true, |
132 | }, | |
133 | end: { | |
134 | description: "End time (Unix Epoch). Default is the last update time.", | |
135 | optional: true, | |
136 | }, | |
137 | }, | |
138 | }, | |
139 | )] | |
392d646f DM |
140 | /// Fetch data from the RRD file |
141 | pub fn fetch_rrd( | |
4cd28918 | 142 | path: String, |
2f942833 | 143 | cf: AggregationFn, |
4cd28918 DM |
144 | resolution: u64, |
145 | start: Option<u64>, | |
146 | end: Option<u64>, | |
147 | ) -> Result<(), Error> { | |
2f942833 | 148 | let rrd = Database::load(&PathBuf::from(path), false)?; |
4cd28918 DM |
149 | |
150 | let data = rrd.extract_data(cf, resolution, start, end)?; | |
151 | ||
152 | println!("{}", serde_json::to_string_pretty(&data)?); | |
153 | ||
154 | Ok(()) | |
155 | } | |
156 | ||
392d646f DM |
157 | #[api( |
158 | input: { | |
159 | properties: { | |
160 | path: { | |
161 | description: "The filename." | |
162 | }, | |
163 | "rra-index": { | |
164 | schema: RRA_INDEX_SCHEMA, | |
165 | }, | |
166 | }, | |
167 | }, | |
168 | )] | |
169 | /// Return the Unix timestamp of the first time slot inside the | |
170 | /// specified RRA (slot start time) | |
d5b9d1f4 | 171 | pub fn first_update_time(path: String, rra_index: usize) -> Result<(), Error> { |
2f942833 | 172 | let rrd = Database::load(&PathBuf::from(path), false)?; |
392d646f DM |
173 | |
174 | if rra_index >= rrd.rra_list.len() { | |
175 | bail!("rra-index is out of range"); | |
176 | } | |
177 | let rra = &rrd.rra_list[rra_index]; | |
d5b9d1f4 | 178 | let duration = (rra.data.len() as u64) * rra.resolution; |
392d646f DM |
179 | let first = rra.slot_start_time((rrd.source.last_update as u64).saturating_sub(duration)); |
180 | ||
181 | println!("{}", first); | |
182 | Ok(()) | |
183 | } | |
184 | ||
185 | #[api( | |
186 | input: { | |
187 | properties: { | |
188 | path: { | |
189 | description: "The filename." | |
190 | }, | |
191 | }, | |
192 | }, | |
193 | )] | |
194 | /// Return the Unix timestamp of the last update | |
195 | pub fn last_update_time(path: String) -> Result<(), Error> { | |
2f942833 | 196 | let rrd = Database::load(&PathBuf::from(path), false)?; |
392d646f DM |
197 | |
198 | println!("{}", rrd.source.last_update); | |
199 | Ok(()) | |
200 | } | |
201 | ||
202 | #[api( | |
203 | input: { | |
204 | properties: { | |
205 | path: { | |
206 | description: "The filename." | |
207 | }, | |
208 | }, | |
209 | }, | |
210 | )] | |
211 | /// Return the time and value from the last update | |
212 | pub fn last_update(path: String) -> Result<(), Error> { | |
2f942833 | 213 | let rrd = Database::load(&PathBuf::from(path), false)?; |
392d646f DM |
214 | |
215 | let result = json!({ | |
216 | "time": rrd.source.last_update, | |
217 | "value": rrd.source.last_value, | |
218 | }); | |
219 | ||
220 | println!("{}", serde_json::to_string_pretty(&result)?); | |
221 | ||
222 | Ok(()) | |
223 | } | |
224 | ||
4cd28918 DM |
225 | #[api( |
226 | input: { | |
227 | properties: { | |
228 | dst: { | |
724c3dda | 229 | type: DataSourceType, |
4cd28918 DM |
230 | }, |
231 | path: { | |
232 | description: "The filename to create." | |
233 | }, | |
234 | rra: { | |
235 | description: "Configuration of contained RRAs.", | |
236 | type: Array, | |
237 | items: { | |
238 | schema: RRA_CONFIG_STRING_SCHEMA, | |
239 | } | |
240 | }, | |
241 | }, | |
242 | }, | |
243 | )] | |
392d646f | 244 | /// Create a new RRD file |
2f942833 | 245 | pub fn create_rrd(dst: DataSourceType, path: String, rra: Vec<String>) -> Result<(), Error> { |
4cd28918 DM |
246 | let mut rra_list = Vec::new(); |
247 | ||
248 | for item in rra.iter() { | |
d5b9d1f4 TL |
249 | let rra: RRAConfig = |
250 | serde_json::from_value(RRAConfig::API_SCHEMA.parse_property_string(item)?)?; | |
4cd28918 | 251 | println!("GOT {:?}", rra); |
2f942833 | 252 | rra_list.push(Archive::new(rra.cf, rra.r, rra.n as usize)); |
4cd28918 DM |
253 | } |
254 | ||
255 | let path = PathBuf::from(path); | |
256 | ||
2f942833 | 257 | let rrd = Database::new(dst, rra_list); |
4cd28918 | 258 | |
a7ee3455 | 259 | rrd.save(&path, CreateOptions::new(), false)?; |
4cd28918 DM |
260 | |
261 | Ok(()) | |
262 | } | |
263 | ||
392d646f DM |
264 | #[api( |
265 | input: { | |
266 | properties: { | |
267 | path: { | |
268 | description: "The filename." | |
269 | }, | |
270 | "rra-index": { | |
271 | schema: RRA_INDEX_SCHEMA, | |
272 | }, | |
273 | slots: { | |
274 | description: "The number of slots you want to add or remove.", | |
275 | type: i64, | |
276 | }, | |
277 | }, | |
278 | }, | |
279 | )] | |
280 | /// Resize. Change the number of data slots for the specified RRA. | |
d5b9d1f4 | 281 | pub fn resize_rrd(path: String, rra_index: usize, slots: i64) -> Result<(), Error> { |
392d646f DM |
282 | let path = PathBuf::from(&path); |
283 | ||
2f942833 | 284 | let mut rrd = Database::load(&path, false)?; |
392d646f DM |
285 | |
286 | if rra_index >= rrd.rra_list.len() { | |
287 | bail!("rra-index is out of range"); | |
288 | } | |
289 | ||
290 | let rra = &rrd.rra_list[rra_index]; | |
291 | ||
292 | let new_slots = (rra.data.len() as i64) + slots; | |
293 | ||
294 | if new_slots < 1 { | |
109902fb | 295 | bail!("number of new slots is too small ('{}' < 1)", new_slots); |
392d646f DM |
296 | } |
297 | ||
d5b9d1f4 | 298 | if new_slots > 1024 * 1024 { |
109902fb | 299 | bail!("number of new slots is too big ('{}' > 1M)", new_slots); |
392d646f DM |
300 | } |
301 | ||
302 | let rra_end = rra.slot_end_time(rrd.source.last_update as u64); | |
d5b9d1f4 | 303 | let rra_start = rra_end - rra.resolution * (rra.data.len() as u64); |
56b5c289 WB |
304 | let (start, reso, data) = rra |
305 | .extract_data(rra_start, rra_end, rrd.source.last_update) | |
306 | .into(); | |
392d646f | 307 | |
2f942833 | 308 | let mut new_rra = Archive::new(rra.cf, rra.resolution, new_slots as usize); |
392d646f DM |
309 | new_rra.last_count = rra.last_count; |
310 | ||
311 | new_rra.insert_data(start, reso, data)?; | |
312 | ||
313 | rrd.rra_list[rra_index] = new_rra; | |
314 | ||
a7ee3455 | 315 | rrd.save(&path, CreateOptions::new(), false)?; |
392d646f DM |
316 | |
317 | Ok(()) | |
318 | } | |
4cd28918 DM |
319 | |
320 | fn main() -> Result<(), Error> { | |
4cd28918 DM |
321 | let uid = nix::unistd::Uid::current(); |
322 | ||
323 | let username = match nix::unistd::User::from_uid(uid)? { | |
324 | Some(user) => user.name, | |
325 | None => bail!("unable to get user name"), | |
326 | }; | |
327 | ||
328 | let cmd_def = CliCommandMap::new() | |
329 | .insert( | |
330 | "create", | |
392d646f | 331 | CliCommand::new(&API_METHOD_CREATE_RRD) |
4cd28918 | 332 | .arg_param(&["path"]) |
d5b9d1f4 | 333 | .completion_cb("path", complete_file_name), |
4cd28918 DM |
334 | ) |
335 | .insert( | |
392d646f DM |
336 | "dump", |
337 | CliCommand::new(&API_METHOD_DUMP_RRD) | |
4cd28918 | 338 | .arg_param(&["path"]) |
d5b9d1f4 TL |
339 | .completion_cb("path", complete_file_name), |
340 | ) | |
4cd28918 DM |
341 | .insert( |
342 | "fetch", | |
392d646f | 343 | CliCommand::new(&API_METHOD_FETCH_RRD) |
4cd28918 | 344 | .arg_param(&["path"]) |
d5b9d1f4 TL |
345 | .completion_cb("path", complete_file_name), |
346 | ) | |
392d646f DM |
347 | .insert( |
348 | "first", | |
349 | CliCommand::new(&API_METHOD_FIRST_UPDATE_TIME) | |
350 | .arg_param(&["path"]) | |
d5b9d1f4 | 351 | .completion_cb("path", complete_file_name), |
4cd28918 DM |
352 | ) |
353 | .insert( | |
392d646f DM |
354 | "info", |
355 | CliCommand::new(&API_METHOD_RRD_INFO) | |
356 | .arg_param(&["path"]) | |
d5b9d1f4 | 357 | .completion_cb("path", complete_file_name), |
392d646f DM |
358 | ) |
359 | .insert( | |
360 | "last", | |
361 | CliCommand::new(&API_METHOD_LAST_UPDATE_TIME) | |
362 | .arg_param(&["path"]) | |
d5b9d1f4 | 363 | .completion_cb("path", complete_file_name), |
392d646f DM |
364 | ) |
365 | .insert( | |
366 | "lastupdate", | |
367 | CliCommand::new(&API_METHOD_LAST_UPDATE) | |
368 | .arg_param(&["path"]) | |
d5b9d1f4 | 369 | .completion_cb("path", complete_file_name), |
392d646f DM |
370 | ) |
371 | .insert( | |
372 | "resize", | |
373 | CliCommand::new(&API_METHOD_RESIZE_RRD) | |
374 | .arg_param(&["path"]) | |
d5b9d1f4 | 375 | .completion_cb("path", complete_file_name), |
392d646f DM |
376 | ) |
377 | .insert( | |
378 | "update", | |
379 | CliCommand::new(&API_METHOD_UPDATE_RRD) | |
4cd28918 | 380 | .arg_param(&["path"]) |
d5b9d1f4 TL |
381 | .completion_cb("path", complete_file_name), |
382 | ); | |
4cd28918 DM |
383 | |
384 | let mut rpcenv = CliEnvironment::new(); | |
385 | rpcenv.set_auth_id(Some(format!("{}@pam", username))); | |
386 | ||
387 | run_cli_command(cmd_def, rpcenv, None); | |
388 | ||
389 | Ok(()) | |
4cd28918 | 390 | } |