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