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