]>
Commit | Line | Data |
---|---|---|
0c9209b0 DM |
1 | use std::sync::Arc; |
2 | ||
a65e3e4b DC |
3 | use anyhow::Error; |
4 | use serde_json::{json, Value}; | |
5 | ||
6ef1b649 WB |
6 | use proxmox_router::cli::*; |
7 | use proxmox_schema::api; | |
f9a5beaa | 8 | use proxmox_sys::fs::file_get_contents; |
0c9209b0 | 9 | |
db87d93e | 10 | use pbs_api_types::{BackupGroup, CryptMode, SnapshotListItem}; |
2b7f8dd5 | 11 | use pbs_client::tools::key_source::get_encryption_key_password; |
f9a5beaa | 12 | use pbs_config::key_config::decrypt_key; |
db87d93e | 13 | use pbs_datastore::DataBlob; |
f9a5beaa | 14 | use pbs_tools::crypt_config::CryptConfig; |
3c8c2827 | 15 | use pbs_tools::json::required_string_param; |
2b7f8dd5 | 16 | |
a65e3e4b | 17 | use crate::{ |
f9a5beaa TL |
18 | api_datastore_list_snapshots, complete_backup_group, complete_backup_snapshot, |
19 | complete_repository, connect, crypto_parameters, extract_repository_from_value, | |
20 | record_repository, BackupDir, KEYFD_SCHEMA, KEYFILE_SCHEMA, REPO_URL_SCHEMA, | |
a65e3e4b DC |
21 | }; |
22 | ||
731eeef2 DM |
23 | #[api( |
24 | input: { | |
25 | properties: { | |
26 | repository: { | |
27 | schema: REPO_URL_SCHEMA, | |
28 | optional: true, | |
29 | }, | |
30 | group: { | |
31 | type: String, | |
32 | description: "Backup group.", | |
33 | optional: true, | |
34 | }, | |
35 | "output-format": { | |
36 | schema: OUTPUT_FORMAT, | |
37 | optional: true, | |
38 | }, | |
39 | } | |
40 | } | |
41 | )] | |
42 | /// List backup snapshots. | |
43 | async fn list_snapshots(param: Value) -> Result<Value, Error> { | |
731eeef2 DM |
44 | let repo = extract_repository_from_value(¶m)?; |
45 | ||
46 | let output_format = get_output_format(¶m); | |
47 | ||
48 | let client = connect(&repo)?; | |
49 | ||
50 | let group: Option<BackupGroup> = if let Some(path) = param["group"].as_str() { | |
51 | Some(path.parse()?) | |
52 | } else { | |
53 | None | |
54 | }; | |
55 | ||
56 | let mut data = api_datastore_list_snapshots(&client, repo.store(), group).await?; | |
57 | ||
58 | record_repository(&repo); | |
59 | ||
60 | let render_snapshot_path = |_v: &Value, record: &Value| -> Result<String, Error> { | |
61 | let item: SnapshotListItem = serde_json::from_value(record.to_owned())?; | |
db87d93e | 62 | Ok(item.backup.to_string()) |
731eeef2 DM |
63 | }; |
64 | ||
65 | let render_files = |_v: &Value, record: &Value| -> Result<String, Error> { | |
66 | let item: SnapshotListItem = serde_json::from_value(record.to_owned())?; | |
67 | let mut filenames = Vec::new(); | |
68 | for file in &item.files { | |
69 | filenames.push(file.filename.to_string()); | |
70 | } | |
770a36e5 | 71 | Ok(pbs_tools::format::render_backup_file_list(&filenames[..])) |
731eeef2 DM |
72 | }; |
73 | ||
74 | let options = default_table_format_options() | |
75 | .sortby("backup-type", false) | |
76 | .sortby("backup-id", false) | |
77 | .sortby("backup-time", false) | |
f9a5beaa TL |
78 | .column( |
79 | ColumnConfig::new("backup-id") | |
80 | .renderer(render_snapshot_path) | |
81 | .header("snapshot"), | |
82 | ) | |
770a36e5 | 83 | .column(ColumnConfig::new("size").renderer(pbs_tools::format::render_bytes_human_readable)) |
f9a5beaa | 84 | .column(ColumnConfig::new("files").renderer(render_files)); |
731eeef2 | 85 | |
e351ac78 | 86 | let return_type = &pbs_api_types::ADMIN_DATASTORE_LIST_SNAPSHOTS_RETURN_TYPE; |
731eeef2 | 87 | |
b2362a12 | 88 | format_and_print_result_full(&mut data, return_type, &output_format, &options); |
731eeef2 DM |
89 | |
90 | Ok(Value::Null) | |
91 | } | |
92 | ||
61205f00 DM |
93 | #[api( |
94 | input: { | |
95 | properties: { | |
96 | repository: { | |
97 | schema: REPO_URL_SCHEMA, | |
98 | optional: true, | |
99 | }, | |
100 | snapshot: { | |
101 | type: String, | |
102 | description: "Snapshot path.", | |
103 | }, | |
104 | "output-format": { | |
105 | schema: OUTPUT_FORMAT, | |
106 | optional: true, | |
107 | }, | |
108 | } | |
109 | } | |
110 | )] | |
111 | /// List snapshot files. | |
112 | async fn list_snapshot_files(param: Value) -> Result<Value, Error> { | |
61205f00 DM |
113 | let repo = extract_repository_from_value(¶m)?; |
114 | ||
3c8c2827 | 115 | let path = required_string_param(¶m, "snapshot")?; |
61205f00 DM |
116 | let snapshot: BackupDir = path.parse()?; |
117 | ||
118 | let output_format = get_output_format(¶m); | |
119 | ||
120 | let client = connect(&repo)?; | |
121 | ||
122 | let path = format!("api2/json/admin/datastore/{}/files", repo.store()); | |
123 | ||
f9a5beaa TL |
124 | let mut result = client |
125 | .get( | |
126 | &path, | |
127 | Some(json!({ | |
db87d93e WB |
128 | "backup-type": snapshot.group.ty, |
129 | "backup-id": snapshot.group.id, | |
130 | "backup-time": snapshot.time, | |
f9a5beaa TL |
131 | })), |
132 | ) | |
133 | .await?; | |
61205f00 DM |
134 | |
135 | record_repository(&repo); | |
136 | ||
e351ac78 | 137 | let return_type = &pbs_api_types::ADMIN_DATASTORE_LIST_SNAPSHOT_FILES_RETURN_TYPE; |
61205f00 DM |
138 | |
139 | let mut data: Value = result["data"].take(); | |
140 | ||
141 | let options = default_table_format_options(); | |
142 | ||
b2362a12 | 143 | format_and_print_result_full(&mut data, return_type, &output_format, &options); |
61205f00 DM |
144 | |
145 | Ok(Value::Null) | |
146 | } | |
147 | ||
edebd523 DM |
148 | #[api( |
149 | input: { | |
150 | properties: { | |
151 | repository: { | |
152 | schema: REPO_URL_SCHEMA, | |
153 | optional: true, | |
154 | }, | |
155 | snapshot: { | |
156 | type: String, | |
157 | description: "Snapshot path.", | |
158 | }, | |
159 | } | |
160 | } | |
161 | )] | |
162 | /// Forget (remove) backup snapshots. | |
163 | async fn forget_snapshots(param: Value) -> Result<Value, Error> { | |
edebd523 DM |
164 | let repo = extract_repository_from_value(¶m)?; |
165 | ||
3c8c2827 | 166 | let path = required_string_param(¶m, "snapshot")?; |
edebd523 DM |
167 | let snapshot: BackupDir = path.parse()?; |
168 | ||
d4877712 | 169 | let client = connect(&repo)?; |
edebd523 DM |
170 | |
171 | let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store()); | |
172 | ||
f9a5beaa TL |
173 | let result = client |
174 | .delete( | |
175 | &path, | |
176 | Some(json!({ | |
db87d93e WB |
177 | "backup-type": snapshot.group.ty, |
178 | "backup-id": snapshot.group.id, | |
179 | "backup-time": snapshot.time, | |
f9a5beaa TL |
180 | })), |
181 | ) | |
182 | .await?; | |
edebd523 DM |
183 | |
184 | record_repository(&repo); | |
185 | ||
186 | Ok(result) | |
187 | } | |
188 | ||
0c9209b0 DM |
189 | #[api( |
190 | input: { | |
191 | properties: { | |
192 | repository: { | |
193 | schema: REPO_URL_SCHEMA, | |
194 | optional: true, | |
195 | }, | |
196 | snapshot: { | |
197 | type: String, | |
198 | description: "Group/Snapshot path.", | |
199 | }, | |
200 | logfile: { | |
201 | type: String, | |
202 | description: "The path to the log file you want to upload.", | |
203 | }, | |
204 | keyfile: { | |
205 | schema: KEYFILE_SCHEMA, | |
206 | optional: true, | |
207 | }, | |
208 | "keyfd": { | |
209 | schema: KEYFD_SCHEMA, | |
210 | optional: true, | |
211 | }, | |
212 | "crypt-mode": { | |
213 | type: CryptMode, | |
214 | optional: true, | |
215 | }, | |
216 | } | |
217 | } | |
218 | )] | |
219 | /// Upload backup log file. | |
220 | async fn upload_log(param: Value) -> Result<Value, Error> { | |
3c8c2827 | 221 | let logfile = required_string_param(¶m, "logfile")?; |
0c9209b0 DM |
222 | let repo = extract_repository_from_value(¶m)?; |
223 | ||
3c8c2827 | 224 | let snapshot = required_string_param(¶m, "snapshot")?; |
0c9209b0 DM |
225 | let snapshot: BackupDir = snapshot.parse()?; |
226 | ||
d4877712 | 227 | let client = connect(&repo)?; |
0c9209b0 | 228 | |
c6a7ea0a | 229 | let crypto = crypto_parameters(¶m)?; |
0c9209b0 | 230 | |
c6a7ea0a | 231 | let crypt_config = match crypto.enc_key { |
0c9209b0 DM |
232 | None => None, |
233 | Some(key) => { | |
ff8945fd | 234 | let (key, _created, _) = decrypt_key(&key.key, &get_encryption_key_password)?; |
0c9209b0 DM |
235 | let crypt_config = CryptConfig::new(key)?; |
236 | Some(Arc::new(crypt_config)) | |
237 | } | |
238 | }; | |
239 | ||
240 | let data = file_get_contents(logfile)?; | |
241 | ||
242 | // fixme: howto sign log? | |
c6a7ea0a | 243 | let blob = match crypto.mode { |
0c9209b0 | 244 | CryptMode::None | CryptMode::SignOnly => DataBlob::encode(&data, None, true)?, |
f9a5beaa TL |
245 | CryptMode::Encrypt => { |
246 | DataBlob::encode(&data, crypt_config.as_ref().map(Arc::as_ref), true)? | |
247 | } | |
0c9209b0 DM |
248 | }; |
249 | ||
250 | let raw_data = blob.into_inner(); | |
251 | ||
f9a5beaa TL |
252 | let path = format!( |
253 | "api2/json/admin/datastore/{}/upload-backup-log", | |
254 | repo.store() | |
255 | ); | |
0c9209b0 | 256 | |
db87d93e | 257 | let args = serde_json::to_value(&snapshot)?; |
0c9209b0 DM |
258 | let body = hyper::Body::from(raw_data); |
259 | ||
f9a5beaa TL |
260 | client |
261 | .upload("application/octet-stream", body, &path, Some(args)) | |
262 | .await | |
0c9209b0 DM |
263 | } |
264 | ||
a65e3e4b DC |
265 | #[api( |
266 | input: { | |
267 | properties: { | |
268 | repository: { | |
269 | schema: REPO_URL_SCHEMA, | |
270 | optional: true, | |
271 | }, | |
272 | snapshot: { | |
273 | type: String, | |
274 | description: "Snapshot path.", | |
275 | }, | |
276 | "output-format": { | |
277 | schema: OUTPUT_FORMAT, | |
278 | optional: true, | |
279 | }, | |
280 | } | |
281 | } | |
282 | )] | |
283 | /// Show notes | |
284 | async fn show_notes(param: Value) -> Result<Value, Error> { | |
285 | let repo = extract_repository_from_value(¶m)?; | |
3c8c2827 | 286 | let path = required_string_param(¶m, "snapshot")?; |
a65e3e4b DC |
287 | |
288 | let snapshot: BackupDir = path.parse()?; | |
289 | let client = connect(&repo)?; | |
290 | ||
291 | let path = format!("api2/json/admin/datastore/{}/notes", repo.store()); | |
292 | ||
293 | let args = json!({ | |
db87d93e WB |
294 | "backup-type": snapshot.group.ty, |
295 | "backup-id": snapshot.group.id, | |
296 | "backup-time": snapshot.time, | |
a65e3e4b DC |
297 | }); |
298 | ||
299 | let output_format = get_output_format(¶m); | |
300 | ||
301 | let mut result = client.get(&path, Some(args)).await?; | |
302 | ||
303 | let notes = result["data"].take(); | |
304 | ||
305 | if output_format == "text" { | |
306 | if let Some(notes) = notes.as_str() { | |
307 | println!("{}", notes); | |
308 | } | |
309 | } else { | |
310 | format_and_print_result( | |
311 | &json!({ | |
312 | "notes": notes, | |
313 | }), | |
314 | &output_format, | |
315 | ); | |
316 | } | |
317 | ||
318 | Ok(Value::Null) | |
319 | } | |
320 | ||
321 | #[api( | |
322 | input: { | |
323 | properties: { | |
324 | repository: { | |
325 | schema: REPO_URL_SCHEMA, | |
326 | optional: true, | |
327 | }, | |
328 | snapshot: { | |
329 | type: String, | |
330 | description: "Snapshot path.", | |
331 | }, | |
332 | notes: { | |
333 | type: String, | |
334 | description: "The Notes.", | |
335 | }, | |
336 | } | |
337 | } | |
338 | )] | |
339 | /// Update Notes | |
340 | async fn update_notes(param: Value) -> Result<Value, Error> { | |
341 | let repo = extract_repository_from_value(¶m)?; | |
3c8c2827 WB |
342 | let path = required_string_param(¶m, "snapshot")?; |
343 | let notes = required_string_param(¶m, "notes")?; | |
a65e3e4b DC |
344 | |
345 | let snapshot: BackupDir = path.parse()?; | |
d4877712 | 346 | let client = connect(&repo)?; |
a65e3e4b DC |
347 | |
348 | let path = format!("api2/json/admin/datastore/{}/notes", repo.store()); | |
349 | ||
350 | let args = json!({ | |
db87d93e WB |
351 | "backup-type": snapshot.group.ty, |
352 | "backup-id": snapshot.group.id, | |
353 | "backup-time": snapshot.time, | |
a65e3e4b DC |
354 | "notes": notes, |
355 | }); | |
356 | ||
357 | client.put(&path, Some(args)).await?; | |
358 | ||
359 | Ok(Value::Null) | |
360 | } | |
361 | ||
87e17fb4 DC |
362 | #[api( |
363 | input: { | |
364 | properties: { | |
365 | repository: { | |
366 | schema: REPO_URL_SCHEMA, | |
367 | optional: true, | |
368 | }, | |
369 | snapshot: { | |
370 | type: String, | |
371 | description: "Snapshot path.", | |
372 | }, | |
373 | "output-format": { | |
374 | schema: OUTPUT_FORMAT, | |
375 | optional: true, | |
376 | }, | |
377 | } | |
378 | } | |
379 | )] | |
380 | /// Show protection status of the specified snapshot | |
9b5ecbe2 | 381 | async fn show_protection(param: Value) -> Result<(), Error> { |
87e17fb4 DC |
382 | let repo = extract_repository_from_value(¶m)?; |
383 | let path = required_string_param(¶m, "snapshot")?; | |
384 | ||
385 | let snapshot: BackupDir = path.parse()?; | |
386 | let client = connect(&repo)?; | |
387 | ||
388 | let path = format!("api2/json/admin/datastore/{}/protected", repo.store()); | |
389 | ||
390 | let args = json!({ | |
db87d93e WB |
391 | "backup-type": snapshot.group.ty, |
392 | "backup-id": snapshot.group.id, | |
393 | "backup-time": snapshot.time, | |
87e17fb4 DC |
394 | }); |
395 | ||
396 | let output_format = get_output_format(¶m); | |
397 | ||
398 | let mut result = client.get(&path, Some(args)).await?; | |
399 | ||
400 | let protected = result["data"].take(); | |
401 | ||
402 | if output_format == "text" { | |
403 | if let Some(protected) = protected.as_bool() { | |
404 | println!("{}", protected); | |
405 | } | |
406 | } else { | |
407 | format_and_print_result( | |
408 | &json!({ | |
409 | "protected": protected, | |
410 | }), | |
411 | &output_format, | |
412 | ); | |
413 | } | |
414 | ||
9b5ecbe2 | 415 | Ok(()) |
87e17fb4 DC |
416 | } |
417 | ||
418 | #[api( | |
419 | input: { | |
420 | properties: { | |
421 | repository: { | |
422 | schema: REPO_URL_SCHEMA, | |
423 | optional: true, | |
424 | }, | |
425 | snapshot: { | |
426 | type: String, | |
427 | description: "Snapshot path.", | |
428 | }, | |
429 | protected: { | |
430 | type: bool, | |
431 | description: "The protection status.", | |
432 | }, | |
433 | } | |
434 | } | |
435 | )] | |
436 | /// Update Protection Status of a snapshot | |
9b5ecbe2 | 437 | async fn update_protection(protected: bool, param: Value) -> Result<(), Error> { |
87e17fb4 DC |
438 | let repo = extract_repository_from_value(¶m)?; |
439 | let path = required_string_param(¶m, "snapshot")?; | |
440 | ||
441 | let snapshot: BackupDir = path.parse()?; | |
d4877712 | 442 | let client = connect(&repo)?; |
87e17fb4 DC |
443 | |
444 | let path = format!("api2/json/admin/datastore/{}/protected", repo.store()); | |
445 | ||
446 | let args = json!({ | |
db87d93e WB |
447 | "backup-type": snapshot.group.ty, |
448 | "backup-id": snapshot.group.id, | |
449 | "backup-time": snapshot.time, | |
87e17fb4 DC |
450 | "protected": protected, |
451 | }); | |
452 | ||
453 | client.put(&path, Some(args)).await?; | |
454 | ||
9b5ecbe2 | 455 | Ok(()) |
87e17fb4 DC |
456 | } |
457 | ||
458 | fn protected_cli() -> CliCommandMap { | |
459 | CliCommandMap::new() | |
460 | .insert( | |
461 | "show", | |
462 | CliCommand::new(&API_METHOD_SHOW_PROTECTION) | |
463 | .arg_param(&["snapshot"]) | |
464 | .completion_cb("snapshot", complete_backup_snapshot), | |
465 | ) | |
466 | .insert( | |
467 | "update", | |
468 | CliCommand::new(&API_METHOD_UPDATE_PROTECTION) | |
469 | .arg_param(&["snapshot", "protected"]) | |
470 | .completion_cb("snapshot", complete_backup_snapshot), | |
471 | ) | |
472 | } | |
473 | ||
a65e3e4b DC |
474 | fn notes_cli() -> CliCommandMap { |
475 | CliCommandMap::new() | |
476 | .insert( | |
477 | "show", | |
478 | CliCommand::new(&API_METHOD_SHOW_NOTES) | |
479 | .arg_param(&["snapshot"]) | |
480 | .completion_cb("snapshot", complete_backup_snapshot), | |
481 | ) | |
482 | .insert( | |
483 | "update", | |
484 | CliCommand::new(&API_METHOD_UPDATE_NOTES) | |
485 | .arg_param(&["snapshot", "notes"]) | |
486 | .completion_cb("snapshot", complete_backup_snapshot), | |
487 | ) | |
488 | } | |
489 | ||
490 | pub fn snapshot_mgtm_cli() -> CliCommandMap { | |
731eeef2 DM |
491 | CliCommandMap::new() |
492 | .insert("notes", notes_cli()) | |
87e17fb4 | 493 | .insert("protected", protected_cli()) |
731eeef2 | 494 | .insert( |
61205f00 DM |
495 | "list", |
496 | CliCommand::new(&API_METHOD_LIST_SNAPSHOTS) | |
731eeef2 DM |
497 | .arg_param(&["group"]) |
498 | .completion_cb("group", complete_backup_group) | |
f9a5beaa | 499 | .completion_cb("repository", complete_repository), |
731eeef2 | 500 | ) |
61205f00 DM |
501 | .insert( |
502 | "files", | |
503 | CliCommand::new(&API_METHOD_LIST_SNAPSHOT_FILES) | |
504 | .arg_param(&["snapshot"]) | |
505 | .completion_cb("repository", complete_repository) | |
f9a5beaa | 506 | .completion_cb("snapshot", complete_backup_snapshot), |
61205f00 | 507 | ) |
edebd523 DM |
508 | .insert( |
509 | "forget", | |
510 | CliCommand::new(&API_METHOD_FORGET_SNAPSHOTS) | |
511 | .arg_param(&["snapshot"]) | |
512 | .completion_cb("repository", complete_repository) | |
f9a5beaa | 513 | .completion_cb("snapshot", complete_backup_snapshot), |
edebd523 | 514 | ) |
0c9209b0 DM |
515 | .insert( |
516 | "upload-log", | |
517 | CliCommand::new(&API_METHOD_UPLOAD_LOG) | |
518 | .arg_param(&["snapshot", "logfile"]) | |
519 | .completion_cb("snapshot", complete_backup_snapshot) | |
b3f279e2 DM |
520 | .completion_cb("logfile", complete_file_name) |
521 | .completion_cb("keyfile", complete_file_name) | |
f9a5beaa | 522 | .completion_cb("repository", complete_repository), |
0c9209b0 | 523 | ) |
a65e3e4b | 524 | } |