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