]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/proxmox-tape.rs
f49857dd825381c6be2663ff178d7f5d8576a5d4
[proxmox-backup.git] / src / bin / proxmox-tape.rs
1 use std::collections::HashMap;
2
3 use anyhow::{bail, format_err, Error};
4 use serde_json::{json, Value};
5
6 use proxmox_io::ReadExt;
7 use proxmox_router::RpcEnvironment;
8 use proxmox_router::cli::*;
9 use proxmox_schema::api;
10 use proxmox_section_config::SectionConfigData;
11 use proxmox_time::strftime_local;
12
13 use pbs_client::view_task_result;
14 use pbs_tools::format::{
15 HumanByte,
16 render_epoch,
17 render_bytes_human_readable,
18 };
19
20 use pbs_config::drive::complete_drive_name;
21 use pbs_config::media_pool::complete_pool_name;
22 use pbs_config::datastore::complete_datastore_name;
23
24 use pbs_api_types::{
25 Userid, Authid, DATASTORE_SCHEMA, DATASTORE_MAP_LIST_SCHEMA,
26 DRIVE_NAME_SCHEMA, MEDIA_LABEL_SCHEMA, MEDIA_POOL_NAME_SCHEMA,
27 TAPE_RESTORE_SNAPSHOT_SCHEMA, GROUP_FILTER_LIST_SCHEMA, GroupListItem,
28 };
29 use pbs_tape::{
30 PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0, BlockReadError, MediaContentHeader,
31 };
32
33 use proxmox_backup::{
34 api2,
35 tape::{
36 drive::{
37 open_drive,
38 lock_tape_device,
39 set_tape_device_state,
40 },
41 complete_media_label_text,
42 complete_media_set_uuid,
43 complete_media_set_snapshots,
44 file_formats::{
45 proxmox_tape_magic_to_text,
46 },
47 },
48 client_helpers::connect_to_localhost,
49 };
50
51 mod proxmox_tape;
52 use proxmox_tape::*;
53
54 async fn get_backup_groups(store: &str) -> Result<Vec<GroupListItem>, Error> {
55 let client = connect_to_localhost()?;
56 let api_res = client
57 .get(&format!("api2/json/admin/datastore/{}/groups", store), None)
58 .await?;
59
60 match api_res.get("data") {
61 Some(data) => Ok(serde_json::from_value::<Vec<GroupListItem>>(data.to_owned())?),
62 None => bail!("could not get group list"),
63 }
64 }
65
66 // shell completion helper
67 pub fn complete_datastore_group_filter(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
68
69 let mut list = Vec::new();
70
71 list.push("regex:".to_string());
72 list.push("type:ct".to_string());
73 list.push("type:host".to_string());
74 list.push("type:vm".to_string());
75
76 if let Some(store) = param.get("store") {
77 let groups = pbs_runtime::block_on(async { get_backup_groups(store).await });
78 if let Ok(groups) = groups {
79 list.extend(groups.iter().map(|group| format!("group:{}/{}", group.backup_type, group.backup_id)));
80 }
81 }
82
83 list
84 }
85
86 pub fn extract_drive_name(
87 param: &mut Value,
88 config: &SectionConfigData,
89 ) -> Result<String, Error> {
90
91 let drive = param["drive"]
92 .as_str()
93 .map(String::from)
94 .or_else(|| std::env::var("PROXMOX_TAPE_DRIVE").ok())
95 .or_else(|| {
96
97 let mut drive_names = Vec::new();
98
99 for (name, (section_type, _)) in config.sections.iter() {
100
101 if !(section_type == "linux" || section_type == "virtual") { continue; }
102 drive_names.push(name);
103 }
104
105 if drive_names.len() == 1 {
106 Some(drive_names[0].to_owned())
107 } else {
108 None
109 }
110 })
111 .ok_or_else(|| format_err!("unable to get (default) drive name"))?;
112
113 if let Some(map) = param.as_object_mut() {
114 map.remove("drive");
115 }
116
117 Ok(drive)
118 }
119
120 #[api(
121 input: {
122 properties: {
123 drive: {
124 schema: DRIVE_NAME_SCHEMA,
125 optional: true,
126 },
127 fast: {
128 description: "Use fast erase.",
129 type: bool,
130 optional: true,
131 default: true,
132 },
133 "output-format": {
134 schema: OUTPUT_FORMAT,
135 optional: true,
136 },
137 },
138 },
139 )]
140 /// Format media
141 async fn format_media(mut param: Value) -> Result<(), Error> {
142
143 let output_format = extract_output_format(&mut param);
144
145 let (config, _digest) = pbs_config::drive::config()?;
146
147 let drive = extract_drive_name(&mut param, &config)?;
148
149 let mut client = connect_to_localhost()?;
150
151 let path = format!("api2/json/tape/drive/{}/format-media", drive);
152 let result = client.post(&path, Some(param)).await?;
153
154 view_task_result(&mut client, result, &output_format).await?;
155
156 Ok(())
157 }
158
159 #[api(
160 input: {
161 properties: {
162 drive: {
163 schema: DRIVE_NAME_SCHEMA,
164 optional: true,
165 },
166 "output-format": {
167 schema: OUTPUT_FORMAT,
168 optional: true,
169 },
170 },
171 },
172 )]
173 /// Rewind tape
174 async fn rewind(mut param: Value) -> Result<(), Error> {
175
176 let output_format = extract_output_format(&mut param);
177
178 let (config, _digest) = pbs_config::drive::config()?;
179
180 let drive = extract_drive_name(&mut param, &config)?;
181
182 let mut client = connect_to_localhost()?;
183
184 let path = format!("api2/json/tape/drive/{}/rewind", drive);
185 let result = client.post(&path, Some(param)).await?;
186
187 view_task_result(&mut client, result, &output_format).await?;
188
189 Ok(())
190 }
191
192 #[api(
193 input: {
194 properties: {
195 drive: {
196 schema: DRIVE_NAME_SCHEMA,
197 optional: true,
198 },
199 "output-format": {
200 schema: OUTPUT_FORMAT,
201 optional: true,
202 },
203 },
204 },
205 )]
206 /// Eject/Unload drive media
207 async fn eject_media(mut param: Value) -> Result<(), Error> {
208
209 let output_format = extract_output_format(&mut param);
210
211 let (config, _digest) = pbs_config::drive::config()?;
212
213 let drive = extract_drive_name(&mut param, &config)?;
214
215 let mut client = connect_to_localhost()?;
216
217 let path = format!("api2/json/tape/drive/{}/eject-media", drive);
218 let result = client.post(&path, Some(param)).await?;
219
220 view_task_result(&mut client, result, &output_format).await?;
221
222 Ok(())
223 }
224
225 #[api(
226 input: {
227 properties: {
228 drive: {
229 schema: DRIVE_NAME_SCHEMA,
230 optional: true,
231 },
232 "label-text": {
233 schema: MEDIA_LABEL_SCHEMA,
234 },
235 "output-format": {
236 schema: OUTPUT_FORMAT,
237 optional: true,
238 },
239 },
240 },
241 )]
242 /// Load media with specified label
243 async fn load_media(mut param: Value) -> Result<(), Error> {
244
245 let output_format = extract_output_format(&mut param);
246
247 let (config, _digest) = pbs_config::drive::config()?;
248
249 let drive = extract_drive_name(&mut param, &config)?;
250
251 let mut client = connect_to_localhost()?;
252
253 let path = format!("api2/json/tape/drive/{}/load-media", drive);
254 let result = client.post(&path, Some(param)).await?;
255
256 view_task_result(&mut client, result, &output_format).await?;
257
258 Ok(())
259 }
260
261 #[api(
262 input: {
263 properties: {
264 drive: {
265 schema: DRIVE_NAME_SCHEMA,
266 optional: true,
267 },
268 "label-text": {
269 schema: MEDIA_LABEL_SCHEMA,
270 },
271 },
272 },
273 )]
274 /// Export media with specified label
275 async fn export_media(mut param: Value) -> Result<(), Error> {
276
277 let (config, _digest) = pbs_config::drive::config()?;
278
279 let drive = extract_drive_name(&mut param, &config)?;
280
281 let mut client = connect_to_localhost()?;
282
283 let path = format!("api2/json/tape/drive/{}/export-media", drive);
284 client.put(&path, Some(param)).await?;
285
286 Ok(())
287 }
288
289 #[api(
290 input: {
291 properties: {
292 drive: {
293 schema: DRIVE_NAME_SCHEMA,
294 optional: true,
295 },
296 "source-slot": {
297 description: "Source slot number.",
298 type: u64,
299 minimum: 1,
300 },
301 },
302 },
303 )]
304 /// Load media from the specified slot
305 async fn load_media_from_slot(mut param: Value) -> Result<(), Error> {
306
307 let (config, _digest) = pbs_config::drive::config()?;
308
309 let drive = extract_drive_name(&mut param, &config)?;
310
311 let mut client = connect_to_localhost()?;
312
313 let path = format!("api2/json/tape/drive/{}/load-slot", drive);
314 client.put(&path, Some(param)).await?;
315
316 Ok(())
317 }
318
319 #[api(
320 input: {
321 properties: {
322 drive: {
323 schema: DRIVE_NAME_SCHEMA,
324 optional: true,
325 },
326 "target-slot": {
327 description: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.",
328 type: u64,
329 minimum: 1,
330 optional: true,
331 },
332 "output-format": {
333 schema: OUTPUT_FORMAT,
334 optional: true,
335 },
336 },
337 },
338 )]
339 /// Unload media via changer
340 async fn unload_media(mut param: Value) -> Result<(), Error> {
341
342 let output_format = extract_output_format(&mut param);
343
344 let (config, _digest) = pbs_config::drive::config()?;
345
346 let drive = extract_drive_name(&mut param, &config)?;
347
348 let mut client = connect_to_localhost()?;
349
350 let path = format!("api2/json/tape/drive/{}/unload", drive);
351 let result = client.post(&path, Some(param)).await?;
352
353 view_task_result(&mut client, result, &output_format).await?;
354
355 Ok(())
356 }
357
358 #[api(
359 input: {
360 properties: {
361 pool: {
362 schema: MEDIA_POOL_NAME_SCHEMA,
363 optional: true,
364 },
365 drive: {
366 schema: DRIVE_NAME_SCHEMA,
367 optional: true,
368 },
369 "label-text": {
370 schema: MEDIA_LABEL_SCHEMA,
371 },
372 "output-format": {
373 schema: OUTPUT_FORMAT,
374 optional: true,
375 },
376 },
377 },
378 )]
379 /// Label media
380 async fn label_media(mut param: Value) -> Result<(), Error> {
381
382 let output_format = extract_output_format(&mut param);
383
384 let (config, _digest) = pbs_config::drive::config()?;
385
386 let drive = extract_drive_name(&mut param, &config)?;
387
388 let mut client = connect_to_localhost()?;
389
390 let path = format!("api2/json/tape/drive/{}/label-media", drive);
391 let result = client.post(&path, Some(param)).await?;
392
393 view_task_result(&mut client, result, &output_format).await?;
394
395 Ok(())
396 }
397
398 #[api(
399 input: {
400 properties: {
401 drive: {
402 schema: DRIVE_NAME_SCHEMA,
403 optional: true,
404 },
405 inventorize: {
406 description: "Inventorize media",
407 type: bool,
408 optional: true,
409 },
410 "output-format": {
411 schema: OUTPUT_FORMAT,
412 optional: true,
413 },
414 },
415 },
416 )]
417 /// Read media label
418 async fn read_label(mut param: Value) -> Result<(), Error> {
419
420 let output_format = extract_output_format(&mut param);
421
422 let (config, _digest) = pbs_config::drive::config()?;
423
424 let drive = extract_drive_name(&mut param, &config)?;
425
426 let client = connect_to_localhost()?;
427
428 let path = format!("api2/json/tape/drive/{}/read-label", drive);
429 let mut result = client.get(&path, Some(param)).await?;
430 let mut data = result["data"].take();
431
432 let info = &api2::tape::drive::API_METHOD_READ_LABEL;
433
434 let options = default_table_format_options()
435 .column(ColumnConfig::new("label-text"))
436 .column(ColumnConfig::new("uuid"))
437 .column(ColumnConfig::new("ctime").renderer(render_epoch))
438 .column(ColumnConfig::new("pool"))
439 .column(ColumnConfig::new("media-set-uuid"))
440 .column(ColumnConfig::new("media-set-ctime").renderer(render_epoch))
441 .column(ColumnConfig::new("encryption-key-fingerprint"))
442 ;
443
444 format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
445
446 Ok(())
447 }
448
449 #[api(
450 input: {
451 properties: {
452 "output-format": {
453 schema: OUTPUT_FORMAT,
454 optional: true,
455 },
456 drive: {
457 schema: DRIVE_NAME_SCHEMA,
458 optional: true,
459 },
460 "read-labels": {
461 description: "Load unknown tapes and try read labels",
462 type: bool,
463 optional: true,
464 },
465 "read-all-labels": {
466 description: "Load all tapes and try read labels (even if already inventoried)",
467 type: bool,
468 optional: true,
469 },
470 },
471 },
472 )]
473 /// List (and update) media labels (Changer Inventory)
474 async fn inventory(
475 read_labels: Option<bool>,
476 read_all_labels: Option<bool>,
477 mut param: Value,
478 ) -> Result<(), Error> {
479
480 let output_format = extract_output_format(&mut param);
481
482 let (config, _digest) = pbs_config::drive::config()?;
483 let drive = extract_drive_name(&mut param, &config)?;
484
485 let do_read = read_labels.unwrap_or(false) || read_all_labels.unwrap_or(false);
486
487 let mut client = connect_to_localhost()?;
488
489 let path = format!("api2/json/tape/drive/{}/inventory", drive);
490
491 if do_read {
492
493 let mut param = json!({});
494 if let Some(true) = read_all_labels {
495 param["read-all-labels"] = true.into();
496 }
497
498 let result = client.put(&path, Some(param)).await?; // update inventory
499 view_task_result(&mut client, result, &output_format).await?;
500 }
501
502 let mut result = client.get(&path, None).await?;
503 let mut data = result["data"].take();
504
505 let info = &api2::tape::drive::API_METHOD_INVENTORY;
506
507 let options = default_table_format_options()
508 .column(ColumnConfig::new("label-text"))
509 .column(ColumnConfig::new("uuid"))
510 ;
511
512 format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
513
514 Ok(())
515 }
516
517 #[api(
518 input: {
519 properties: {
520 pool: {
521 schema: MEDIA_POOL_NAME_SCHEMA,
522 optional: true,
523 },
524 drive: {
525 schema: DRIVE_NAME_SCHEMA,
526 optional: true,
527 },
528 "output-format": {
529 schema: OUTPUT_FORMAT,
530 optional: true,
531 },
532 },
533 },
534 )]
535 /// Label media with barcodes from changer device
536 async fn barcode_label_media(mut param: Value) -> Result<(), Error> {
537
538 let output_format = extract_output_format(&mut param);
539
540 let (config, _digest) = pbs_config::drive::config()?;
541
542 let drive = extract_drive_name(&mut param, &config)?;
543
544 let mut client = connect_to_localhost()?;
545
546 let path = format!("api2/json/tape/drive/{}/barcode-label-media", drive);
547 let result = client.post(&path, Some(param)).await?;
548
549 view_task_result(&mut client, result, &output_format).await?;
550
551 Ok(())
552 }
553
554 #[api(
555 input: {
556 properties: {
557 drive: {
558 schema: DRIVE_NAME_SCHEMA,
559 optional: true,
560 },
561 },
562 },
563 )]
564 /// Move to end of media (MTEOM, used to debug)
565 fn move_to_eom(mut param: Value) -> Result<(), Error> {
566
567 let (config, _digest) = pbs_config::drive::config()?;
568
569 let drive = extract_drive_name(&mut param, &config)?;
570
571 let _lock = lock_tape_device(&config, &drive)?;
572 set_tape_device_state(&drive, "moving to eom")?;
573
574 let mut drive = open_drive(&config, &drive)?;
575
576 drive.move_to_eom(false)?;
577
578 Ok(())
579 }
580
581 #[api(
582 input: {
583 properties: {
584 drive: {
585 schema: DRIVE_NAME_SCHEMA,
586 optional: true,
587 },
588 },
589 },
590 )]
591 /// Rewind, then read media contents and print debug info
592 ///
593 /// Note: This reads unless the driver returns an IO Error, so this
594 /// method is expected to fails when we reach EOT.
595 fn debug_scan(mut param: Value) -> Result<(), Error> {
596
597 let (config, _digest) = pbs_config::drive::config()?;
598
599 let drive = extract_drive_name(&mut param, &config)?;
600
601 let _lock = lock_tape_device(&config, &drive)?;
602 set_tape_device_state(&drive, "debug scan")?;
603
604 let mut drive = open_drive(&config, &drive)?;
605
606 println!("rewinding tape");
607 drive.rewind()?;
608
609 loop {
610 let file_number = drive.current_file_number()?;
611
612 match drive.read_next_file() {
613 Err(BlockReadError::EndOfFile) => {
614 println!("filemark number {}", file_number);
615 continue;
616 }
617 Err(BlockReadError::EndOfStream) => {
618 println!("got EOT");
619 return Ok(());
620 }
621 Err(BlockReadError::Error(err)) => {
622 return Err(err.into());
623 }
624 Ok(mut reader) => {
625 println!("got file number {}", file_number);
626
627 let header: Result<MediaContentHeader, _> = unsafe { reader.read_le_value() };
628 match header {
629 Ok(header) => {
630 if header.magic != PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0 {
631 println!("got MediaContentHeader with wrong magic: {:?}", header.magic);
632 } else if let Some(name) = proxmox_tape_magic_to_text(&header.content_magic) {
633 println!("got content header: {}", name);
634 println!(" uuid: {}", header.content_uuid());
635 println!(" ctime: {}", strftime_local("%c", header.ctime)?);
636 println!(" hsize: {}", HumanByte::from(header.size as usize));
637 println!(" part: {}", header.part_number);
638 } else {
639 println!("got unknown content header: {:?}", header.content_magic);
640 }
641 }
642 Err(err) => {
643 println!("unable to read content header - {}", err);
644 }
645 }
646 let bytes = reader.skip_data()?;
647 println!("skipped {}", HumanByte::from(bytes));
648 if let Ok(true) = reader.has_end_marker() {
649 if reader.is_incomplete()? {
650 println!("WARNING: file is incomplete");
651 }
652 } else {
653 println!("WARNING: file without end marker");
654 }
655 }
656 }
657 }
658 }
659
660 #[api(
661 input: {
662 properties: {
663 drive: {
664 schema: DRIVE_NAME_SCHEMA,
665 optional: true,
666 },
667 "output-format": {
668 schema: OUTPUT_FORMAT,
669 optional: true,
670 },
671 },
672 },
673 )]
674 /// Read Cartridge Memory (Medium auxiliary memory attributes)
675 async fn cartridge_memory(mut param: Value) -> Result<(), Error> {
676
677 let output_format = extract_output_format(&mut param);
678
679 let (config, _digest) = pbs_config::drive::config()?;
680
681 let drive = extract_drive_name(&mut param, &config)?;
682
683 let client = connect_to_localhost()?;
684
685 let path = format!("api2/json/tape/drive/{}/cartridge-memory", drive);
686 let mut result = client.get(&path, Some(param)).await?;
687 let mut data = result["data"].take();
688
689 let info = &api2::tape::drive::API_METHOD_CARTRIDGE_MEMORY;
690
691 let options = default_table_format_options()
692 .column(ColumnConfig::new("id"))
693 .column(ColumnConfig::new("name"))
694 .column(ColumnConfig::new("value"))
695 ;
696
697 format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
698 Ok(())
699 }
700
701 #[api(
702 input: {
703 properties: {
704 drive: {
705 schema: DRIVE_NAME_SCHEMA,
706 optional: true,
707 },
708 "output-format": {
709 schema: OUTPUT_FORMAT,
710 optional: true,
711 },
712 },
713 },
714 )]
715 /// Read Volume Statistics (SCSI log page 17h)
716 async fn volume_statistics(mut param: Value) -> Result<(), Error> {
717
718 let output_format = extract_output_format(&mut param);
719
720 let (config, _digest) = pbs_config::drive::config()?;
721
722 let drive = extract_drive_name(&mut param, &config)?;
723
724 let client = connect_to_localhost()?;
725
726 let path = format!("api2/json/tape/drive/{}/volume-statistics", drive);
727 let mut result = client.get(&path, Some(param)).await?;
728 let mut data = result["data"].take();
729
730 let info = &api2::tape::drive::API_METHOD_VOLUME_STATISTICS;
731
732 let options = default_table_format_options();
733
734 format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
735
736 Ok(())
737 }
738
739 #[api(
740 input: {
741 properties: {
742 drive: {
743 schema: DRIVE_NAME_SCHEMA,
744 optional: true,
745 },
746 "output-format": {
747 schema: OUTPUT_FORMAT,
748 optional: true,
749 },
750 },
751 },
752 )]
753 /// Get drive/media status
754 async fn status(mut param: Value) -> Result<(), Error> {
755
756 let output_format = extract_output_format(&mut param);
757
758 let (config, _digest) = pbs_config::drive::config()?;
759
760 let drive = extract_drive_name(&mut param, &config)?;
761
762 let client = connect_to_localhost()?;
763
764 let path = format!("api2/json/tape/drive/{}/status", drive);
765 let mut result = client.get(&path, Some(param)).await?;
766 let mut data = result["data"].take();
767
768 let info = &api2::tape::drive::API_METHOD_STATUS;
769
770 let render_percentage = |value: &Value, _record: &Value| {
771 match value.as_f64() {
772 Some(wearout) => Ok(format!("{:.2}%", wearout*100.0)),
773 None => Ok(String::from("ERROR")), // should never happen
774 }
775 };
776
777 let options = default_table_format_options()
778 .column(ColumnConfig::new("blocksize"))
779 .column(ColumnConfig::new("density"))
780 .column(ColumnConfig::new("compression"))
781 .column(ColumnConfig::new("buffer-mode"))
782 .column(ColumnConfig::new("write-protect"))
783 .column(ColumnConfig::new("alert-flags"))
784 .column(ColumnConfig::new("file-number"))
785 .column(ColumnConfig::new("block-number"))
786 .column(ColumnConfig::new("manufactured").renderer(render_epoch))
787 .column(ColumnConfig::new("bytes-written").renderer(render_bytes_human_readable))
788 .column(ColumnConfig::new("bytes-read").renderer(render_bytes_human_readable))
789 .column(ColumnConfig::new("medium-passes"))
790 .column(ColumnConfig::new("medium-wearout").renderer(render_percentage))
791 .column(ColumnConfig::new("volume-mounts"))
792 ;
793
794 format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
795
796 Ok(())
797 }
798
799 #[api(
800 input: {
801 properties: {
802 drive: {
803 schema: DRIVE_NAME_SCHEMA,
804 optional: true,
805 },
806 "output-format": {
807 schema: OUTPUT_FORMAT,
808 optional: true,
809 },
810 },
811 },
812 )]
813 /// Clean drive
814 async fn clean_drive(mut param: Value) -> Result<(), Error> {
815
816 let output_format = extract_output_format(&mut param);
817
818 let (config, _digest) = pbs_config::drive::config()?;
819
820 let drive = extract_drive_name(&mut param, &config)?;
821
822 let mut client = connect_to_localhost()?;
823
824 let path = format!("api2/json/tape/drive/{}/clean", drive);
825 let result = client.put(&path, Some(param)).await?;
826
827 view_task_result(&mut client, result, &output_format).await?;
828
829 Ok(())
830 }
831
832 #[api(
833 input: {
834 properties: {
835
836 // Note: We cannot use TapeBackupJobSetup, because drive needs to be optional here
837 //setup: {
838 // type: TapeBackupJobSetup,
839 // flatten: true,
840 //},
841
842 store: {
843 schema: DATASTORE_SCHEMA,
844 },
845 pool: {
846 schema: MEDIA_POOL_NAME_SCHEMA,
847 },
848 drive: {
849 schema: DRIVE_NAME_SCHEMA,
850 optional: true,
851 },
852 "eject-media": {
853 description: "Eject media upon job completion.",
854 type: bool,
855 optional: true,
856 },
857 "export-media-set": {
858 description: "Export media set upon job completion.",
859 type: bool,
860 optional: true,
861 },
862 "latest-only": {
863 description: "Backup latest snapshots only.",
864 type: bool,
865 optional: true,
866 },
867 "notify-user": {
868 optional: true,
869 type: Userid,
870 },
871 groups: {
872 schema: GROUP_FILTER_LIST_SCHEMA,
873 optional: true,
874 },
875 "force-media-set": {
876 description: "Ignore the allocation policy and start a new media-set.",
877 optional: true,
878 type: bool,
879 default: false,
880 },
881 "output-format": {
882 schema: OUTPUT_FORMAT,
883 optional: true,
884 },
885 },
886 },
887 )]
888 /// Backup datastore to tape media pool
889 async fn backup(mut param: Value) -> Result<(), Error> {
890
891 let output_format = extract_output_format(&mut param);
892
893 let (config, _digest) = pbs_config::drive::config()?;
894
895 param["drive"] = extract_drive_name(&mut param, &config)?.into();
896
897 let mut client = connect_to_localhost()?;
898
899 let result = client.post("api2/json/tape/backup", Some(param)).await?;
900
901 view_task_result(&mut client, result, &output_format).await?;
902
903 Ok(())
904 }
905
906 #[api(
907 input: {
908 properties: {
909 store: {
910 schema: DATASTORE_MAP_LIST_SCHEMA,
911 },
912 drive: {
913 schema: DRIVE_NAME_SCHEMA,
914 optional: true,
915 },
916 "media-set": {
917 description: "Media set UUID.",
918 type: String,
919 },
920 "notify-user": {
921 type: Userid,
922 optional: true,
923 },
924 "snapshots": {
925 description: "List of snapshots.",
926 type: Array,
927 optional: true,
928 items: {
929 schema: TAPE_RESTORE_SNAPSHOT_SCHEMA,
930 },
931 },
932 owner: {
933 type: Authid,
934 optional: true,
935 },
936 "output-format": {
937 schema: OUTPUT_FORMAT,
938 optional: true,
939 },
940 },
941 },
942 )]
943 /// Restore data from media-set
944 async fn restore(mut param: Value) -> Result<(), Error> {
945
946 let output_format = extract_output_format(&mut param);
947
948 let (config, _digest) = pbs_config::drive::config()?;
949
950 param["drive"] = extract_drive_name(&mut param, &config)?.into();
951
952 let mut client = connect_to_localhost()?;
953
954 let result = client.post("api2/json/tape/restore", Some(param)).await?;
955
956 view_task_result(&mut client, result, &output_format).await?;
957
958 Ok(())
959 }
960
961 #[api(
962 input: {
963 properties: {
964 drive: {
965 schema: DRIVE_NAME_SCHEMA,
966 optional: true,
967 },
968 force: {
969 description: "Force overriding existing index.",
970 type: bool,
971 optional: true,
972 },
973 scan: {
974 description: "Re-read the whole tape to reconstruct the catalog instead of restoring saved versions.",
975 type: bool,
976 optional: true,
977 },
978 verbose: {
979 description: "Verbose mode - log all found chunks.",
980 type: bool,
981 optional: true,
982 },
983 "output-format": {
984 schema: OUTPUT_FORMAT,
985 optional: true,
986 },
987 },
988 },
989 )]
990 /// Scan media and record content
991 async fn catalog_media(mut param: Value) -> Result<(), Error> {
992
993 let output_format = extract_output_format(&mut param);
994
995 let (config, _digest) = pbs_config::drive::config()?;
996
997 let drive = extract_drive_name(&mut param, &config)?;
998
999 let mut client = connect_to_localhost()?;
1000
1001 let path = format!("api2/json/tape/drive/{}/catalog", drive);
1002 let result = client.post(&path, Some(param)).await?;
1003
1004 view_task_result(&mut client, result, &output_format).await?;
1005
1006 Ok(())
1007 }
1008
1009 fn main() {
1010
1011 let cmd_def = CliCommandMap::new()
1012 .insert(
1013 "backup",
1014 CliCommand::new(&API_METHOD_BACKUP)
1015 .arg_param(&["store", "pool"])
1016 .completion_cb("drive", complete_drive_name)
1017 .completion_cb("store", complete_datastore_name)
1018 .completion_cb("pool", complete_pool_name)
1019 .completion_cb("groups", complete_datastore_group_filter)
1020 )
1021 .insert(
1022 "restore",
1023 CliCommand::new(&API_METHOD_RESTORE)
1024 .arg_param(&["media-set", "store", "snapshots"])
1025 .completion_cb("store", complete_datastore_name)
1026 .completion_cb("media-set", complete_media_set_uuid)
1027 .completion_cb("snapshots", complete_media_set_snapshots)
1028 )
1029 .insert(
1030 "barcode-label",
1031 CliCommand::new(&API_METHOD_BARCODE_LABEL_MEDIA)
1032 .completion_cb("drive", complete_drive_name)
1033 .completion_cb("pool", complete_pool_name)
1034 )
1035 .insert(
1036 "rewind",
1037 CliCommand::new(&API_METHOD_REWIND)
1038 .completion_cb("drive", complete_drive_name)
1039 )
1040 .insert(
1041 "scan",
1042 CliCommand::new(&API_METHOD_DEBUG_SCAN)
1043 .completion_cb("drive", complete_drive_name)
1044 )
1045 .insert(
1046 "status",
1047 CliCommand::new(&API_METHOD_STATUS)
1048 .completion_cb("drive", complete_drive_name)
1049 )
1050 .insert(
1051 "eod",
1052 CliCommand::new(&API_METHOD_MOVE_TO_EOM)
1053 .completion_cb("drive", complete_drive_name)
1054 )
1055 .insert(
1056 "format",
1057 CliCommand::new(&API_METHOD_FORMAT_MEDIA)
1058 .completion_cb("drive", complete_drive_name)
1059 )
1060 .insert(
1061 "eject",
1062 CliCommand::new(&API_METHOD_EJECT_MEDIA)
1063 .completion_cb("drive", complete_drive_name)
1064 )
1065 .insert(
1066 "inventory",
1067 CliCommand::new(&API_METHOD_INVENTORY)
1068 .completion_cb("drive", complete_drive_name)
1069 )
1070 .insert(
1071 "read-label",
1072 CliCommand::new(&API_METHOD_READ_LABEL)
1073 .completion_cb("drive", complete_drive_name)
1074 )
1075 .insert(
1076 "catalog",
1077 CliCommand::new(&API_METHOD_CATALOG_MEDIA)
1078 .completion_cb("drive", complete_drive_name)
1079 )
1080 .insert(
1081 "cartridge-memory",
1082 CliCommand::new(&API_METHOD_CARTRIDGE_MEMORY)
1083 .completion_cb("drive", complete_drive_name)
1084 )
1085 .insert(
1086 "volume-statistics",
1087 CliCommand::new(&API_METHOD_VOLUME_STATISTICS)
1088 .completion_cb("drive", complete_drive_name)
1089 )
1090 .insert(
1091 "clean",
1092 CliCommand::new(&API_METHOD_CLEAN_DRIVE)
1093 .completion_cb("drive", complete_drive_name)
1094 )
1095 .insert(
1096 "label",
1097 CliCommand::new(&API_METHOD_LABEL_MEDIA)
1098 .completion_cb("drive", complete_drive_name)
1099 .completion_cb("pool", complete_pool_name)
1100
1101 )
1102 .insert("changer", changer_commands())
1103 .insert("drive", drive_commands())
1104 .insert("pool", pool_commands())
1105 .insert("media", media_commands())
1106 .insert("key", encryption_key_commands())
1107 .insert("backup-job", backup_job_commands())
1108 .insert(
1109 "load-media",
1110 CliCommand::new(&API_METHOD_LOAD_MEDIA)
1111 .arg_param(&["label-text"])
1112 .completion_cb("drive", complete_drive_name)
1113 .completion_cb("label-text", complete_media_label_text)
1114 )
1115 .insert(
1116 "load-media-from-slot",
1117 CliCommand::new(&API_METHOD_LOAD_MEDIA_FROM_SLOT)
1118 .arg_param(&["source-slot"])
1119 .completion_cb("drive", complete_drive_name)
1120 )
1121 .insert(
1122 "unload",
1123 CliCommand::new(&API_METHOD_UNLOAD_MEDIA)
1124 .completion_cb("drive", complete_drive_name)
1125 )
1126 .insert(
1127 "export-media",
1128 CliCommand::new(&API_METHOD_EXPORT_MEDIA)
1129 .arg_param(&["label-text"])
1130 .completion_cb("drive", complete_drive_name)
1131 .completion_cb("label-text", complete_media_label_text)
1132 )
1133 ;
1134
1135 let mut rpcenv = CliEnvironment::new();
1136 rpcenv.set_auth_id(Some(String::from("root@pam")));
1137
1138 pbs_runtime::main(run_async_cli_command(cmd_def, rpcenv));
1139 }