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