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