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