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