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