1 use anyhow
::{format_err, Error}
;
2 use serde_json
::{json, Value}
;
9 section_config
::SectionConfigData
,
17 use pbs_client
::view_task_result
;
18 use pbs_tools
::format
::{
21 render_bytes_human_readable
,
24 use pbs_config
::drive
::complete_drive_name
;
25 use pbs_config
::media_pool
::complete_pool_name
;
26 use pbs_config
::datastore
::complete_datastore_name
;
29 Userid
, Authid
, DATASTORE_SCHEMA
, DATASTORE_MAP_LIST_SCHEMA
,
30 DRIVE_NAME_SCHEMA
, MEDIA_LABEL_SCHEMA
, MEDIA_POOL_NAME_SCHEMA
,
31 TAPE_RESTORE_SNAPSHOT_SCHEMA
,
34 PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0
, BlockReadError
, MediaContentHeader
,
43 set_tape_device_state
,
45 complete_media_label_text
,
46 complete_media_set_uuid
,
47 complete_media_set_snapshots
,
49 proxmox_tape_magic_to_text
,
52 client_helpers
::connect_to_localhost
,
58 pub fn extract_drive_name(
60 config
: &SectionConfigData
,
61 ) -> Result
<String
, Error
> {
63 let drive
= param
["drive"]
66 .or_else(|| std
::env
::var("PROXMOX_TAPE_DRIVE").ok())
69 let mut drive_names
= Vec
::new();
71 for (name
, (section_type
, _
)) in config
.sections
.iter() {
73 if !(section_type
== "linux" || section_type
== "virtual") { continue; }
74 drive_names
.push(name
);
77 if drive_names
.len() == 1 {
78 Some(drive_names
[0].to_owned())
83 .ok_or_else(|| format_err
!("unable to get (default) drive name"))?
;
85 if let Some(map
) = param
.as_object_mut() {
96 schema
: DRIVE_NAME_SCHEMA
,
100 description
: "Use fast erase.",
106 schema
: OUTPUT_FORMAT
,
113 async
fn format_media(mut param
: Value
) -> Result
<(), Error
> {
115 let output_format
= extract_output_format(&mut param
);
117 let (config
, _digest
) = pbs_config
::drive
::config()?
;
119 let drive
= extract_drive_name(&mut param
, &config
)?
;
121 let mut client
= connect_to_localhost()?
;
123 let path
= format
!("api2/json/tape/drive/{}/format-media", drive
);
124 let result
= client
.post(&path
, Some(param
)).await?
;
126 view_task_result(&mut client
, result
, &output_format
).await?
;
135 schema
: DRIVE_NAME_SCHEMA
,
139 schema
: OUTPUT_FORMAT
,
146 async
fn rewind(mut param
: Value
) -> Result
<(), Error
> {
148 let output_format
= extract_output_format(&mut param
);
150 let (config
, _digest
) = pbs_config
::drive
::config()?
;
152 let drive
= extract_drive_name(&mut param
, &config
)?
;
154 let mut client
= connect_to_localhost()?
;
156 let path
= format
!("api2/json/tape/drive/{}/rewind", drive
);
157 let result
= client
.post(&path
, Some(param
)).await?
;
159 view_task_result(&mut client
, result
, &output_format
).await?
;
168 schema
: DRIVE_NAME_SCHEMA
,
172 schema
: OUTPUT_FORMAT
,
178 /// Eject/Unload drive media
179 async
fn eject_media(mut param
: Value
) -> Result
<(), Error
> {
181 let output_format
= extract_output_format(&mut param
);
183 let (config
, _digest
) = pbs_config
::drive
::config()?
;
185 let drive
= extract_drive_name(&mut param
, &config
)?
;
187 let mut client
= connect_to_localhost()?
;
189 let path
= format
!("api2/json/tape/drive/{}/eject-media", drive
);
190 let result
= client
.post(&path
, Some(param
)).await?
;
192 view_task_result(&mut client
, result
, &output_format
).await?
;
201 schema
: DRIVE_NAME_SCHEMA
,
205 schema
: MEDIA_LABEL_SCHEMA
,
208 schema
: OUTPUT_FORMAT
,
214 /// Load media with specified label
215 async
fn load_media(mut param
: Value
) -> Result
<(), Error
> {
217 let output_format
= extract_output_format(&mut param
);
219 let (config
, _digest
) = pbs_config
::drive
::config()?
;
221 let drive
= extract_drive_name(&mut param
, &config
)?
;
223 let mut client
= connect_to_localhost()?
;
225 let path
= format
!("api2/json/tape/drive/{}/load-media", drive
);
226 let result
= client
.post(&path
, Some(param
)).await?
;
228 view_task_result(&mut client
, result
, &output_format
).await?
;
237 schema
: DRIVE_NAME_SCHEMA
,
241 schema
: MEDIA_LABEL_SCHEMA
,
246 /// Export media with specified label
247 async
fn export_media(mut param
: Value
) -> Result
<(), Error
> {
249 let (config
, _digest
) = pbs_config
::drive
::config()?
;
251 let drive
= extract_drive_name(&mut param
, &config
)?
;
253 let mut client
= connect_to_localhost()?
;
255 let path
= format
!("api2/json/tape/drive/{}/export-media", drive
);
256 client
.put(&path
, Some(param
)).await?
;
265 schema
: DRIVE_NAME_SCHEMA
,
269 description
: "Source slot number.",
276 /// Load media from the specified slot
277 async
fn load_media_from_slot(mut param
: Value
) -> Result
<(), Error
> {
279 let (config
, _digest
) = pbs_config
::drive
::config()?
;
281 let drive
= extract_drive_name(&mut param
, &config
)?
;
283 let mut client
= connect_to_localhost()?
;
285 let path
= format
!("api2/json/tape/drive/{}/load-slot", drive
);
286 client
.put(&path
, Some(param
)).await?
;
295 schema
: DRIVE_NAME_SCHEMA
,
299 description
: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.",
305 schema
: OUTPUT_FORMAT
,
311 /// Unload media via changer
312 async
fn unload_media(mut param
: Value
) -> Result
<(), Error
> {
314 let output_format
= extract_output_format(&mut param
);
316 let (config
, _digest
) = pbs_config
::drive
::config()?
;
318 let drive
= extract_drive_name(&mut param
, &config
)?
;
320 let mut client
= connect_to_localhost()?
;
322 let path
= format
!("api2/json/tape/drive/{}/unload", drive
);
323 let result
= client
.post(&path
, Some(param
)).await?
;
325 view_task_result(&mut client
, result
, &output_format
).await?
;
334 schema
: MEDIA_POOL_NAME_SCHEMA
,
338 schema
: DRIVE_NAME_SCHEMA
,
342 schema
: MEDIA_LABEL_SCHEMA
,
345 schema
: OUTPUT_FORMAT
,
352 async
fn label_media(mut param
: Value
) -> Result
<(), Error
> {
354 let output_format
= extract_output_format(&mut param
);
356 let (config
, _digest
) = pbs_config
::drive
::config()?
;
358 let drive
= extract_drive_name(&mut param
, &config
)?
;
360 let mut client
= connect_to_localhost()?
;
362 let path
= format
!("api2/json/tape/drive/{}/label-media", drive
);
363 let result
= client
.post(&path
, Some(param
)).await?
;
365 view_task_result(&mut client
, result
, &output_format
).await?
;
374 schema
: DRIVE_NAME_SCHEMA
,
378 description
: "Inventorize media",
383 schema
: OUTPUT_FORMAT
,
390 async
fn read_label(mut param
: Value
) -> Result
<(), Error
> {
392 let output_format
= extract_output_format(&mut param
);
394 let (config
, _digest
) = pbs_config
::drive
::config()?
;
396 let drive
= extract_drive_name(&mut param
, &config
)?
;
398 let client
= connect_to_localhost()?
;
400 let path
= format
!("api2/json/tape/drive/{}/read-label", drive
);
401 let mut result
= client
.get(&path
, Some(param
)).await?
;
402 let mut data
= result
["data"].take();
404 let info
= &api2
::tape
::drive
::API_METHOD_READ_LABEL
;
406 let options
= default_table_format_options()
407 .column(ColumnConfig
::new("label-text"))
408 .column(ColumnConfig
::new("uuid"))
409 .column(ColumnConfig
::new("ctime").renderer(render_epoch
))
410 .column(ColumnConfig
::new("pool"))
411 .column(ColumnConfig
::new("media-set-uuid"))
412 .column(ColumnConfig
::new("media-set-ctime").renderer(render_epoch
))
413 .column(ColumnConfig
::new("encryption-key-fingerprint"))
416 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
425 schema
: OUTPUT_FORMAT
,
429 schema
: DRIVE_NAME_SCHEMA
,
433 description
: "Load unknown tapes and try read labels",
438 description
: "Load all tapes and try read labels (even if already inventoried)",
445 /// List (and update) media labels (Changer Inventory)
447 read_labels
: Option
<bool
>,
448 read_all_labels
: Option
<bool
>,
450 ) -> Result
<(), Error
> {
452 let output_format
= extract_output_format(&mut param
);
454 let (config
, _digest
) = pbs_config
::drive
::config()?
;
455 let drive
= extract_drive_name(&mut param
, &config
)?
;
457 let do_read
= read_labels
.unwrap_or(false) || read_all_labels
.unwrap_or(false);
459 let mut client
= connect_to_localhost()?
;
461 let path
= format
!("api2/json/tape/drive/{}/inventory", drive
);
465 let mut param
= json
!({}
);
466 if let Some(true) = read_all_labels
{
467 param
["read-all-labels"] = true.into();
470 let result
= client
.put(&path
, Some(param
)).await?
; // update inventory
471 view_task_result(&mut client
, result
, &output_format
).await?
;
474 let mut result
= client
.get(&path
, None
).await?
;
475 let mut data
= result
["data"].take();
477 let info
= &api2
::tape
::drive
::API_METHOD_INVENTORY
;
479 let options
= default_table_format_options()
480 .column(ColumnConfig
::new("label-text"))
481 .column(ColumnConfig
::new("uuid"))
484 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
493 schema
: MEDIA_POOL_NAME_SCHEMA
,
497 schema
: DRIVE_NAME_SCHEMA
,
501 schema
: OUTPUT_FORMAT
,
507 /// Label media with barcodes from changer device
508 async
fn barcode_label_media(mut param
: Value
) -> Result
<(), Error
> {
510 let output_format
= extract_output_format(&mut param
);
512 let (config
, _digest
) = pbs_config
::drive
::config()?
;
514 let drive
= extract_drive_name(&mut param
, &config
)?
;
516 let mut client
= connect_to_localhost()?
;
518 let path
= format
!("api2/json/tape/drive/{}/barcode-label-media", drive
);
519 let result
= client
.post(&path
, Some(param
)).await?
;
521 view_task_result(&mut client
, result
, &output_format
).await?
;
530 schema
: DRIVE_NAME_SCHEMA
,
536 /// Move to end of media (MTEOM, used to debug)
537 fn move_to_eom(mut param
: Value
) -> Result
<(), Error
> {
539 let (config
, _digest
) = pbs_config
::drive
::config()?
;
541 let drive
= extract_drive_name(&mut param
, &config
)?
;
543 let _lock
= lock_tape_device(&config
, &drive
)?
;
544 set_tape_device_state(&drive
, "moving to eom")?
;
546 let mut drive
= open_drive(&config
, &drive
)?
;
548 drive
.move_to_eom(false)?
;
557 schema
: DRIVE_NAME_SCHEMA
,
563 /// Rewind, then read media contents and print debug info
565 /// Note: This reads unless the driver returns an IO Error, so this
566 /// method is expected to fails when we reach EOT.
567 fn debug_scan(mut param
: Value
) -> Result
<(), Error
> {
569 let (config
, _digest
) = pbs_config
::drive
::config()?
;
571 let drive
= extract_drive_name(&mut param
, &config
)?
;
573 let _lock
= lock_tape_device(&config
, &drive
)?
;
574 set_tape_device_state(&drive
, "debug scan")?
;
576 let mut drive
= open_drive(&config
, &drive
)?
;
578 println
!("rewinding tape");
582 let file_number
= drive
.current_file_number()?
;
584 match drive
.read_next_file() {
585 Err(BlockReadError
::EndOfFile
) => {
586 println
!("filemark number {}", file_number
);
589 Err(BlockReadError
::EndOfStream
) => {
593 Err(BlockReadError
::Error(err
)) => {
594 return Err(err
.into());
597 println
!("got file number {}", file_number
);
599 let header
: Result
<MediaContentHeader
, _
> = unsafe { reader.read_le_value() }
;
602 if header
.magic
!= PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0
{
603 println
!("got MediaContentHeader with wrong magic: {:?}", header
.magic
);
604 } else if let Some(name
) = proxmox_tape_magic_to_text(&header
.content_magic
) {
605 println
!("got content header: {}", name
);
606 println
!(" uuid: {}", header
.content_uuid());
607 println
!(" ctime: {}", strftime_local("%c", header
.ctime
)?
);
608 println
!(" hsize: {}", HumanByte
::from(header
.size
as usize));
609 println
!(" part: {}", header
.part_number
);
611 println
!("got unknown content header: {:?}", header
.content_magic
);
615 println
!("unable to read content header - {}", err
);
618 let bytes
= reader
.skip_data()?
;
619 println
!("skipped {}", HumanByte
::from(bytes
));
620 if let Ok(true) = reader
.has_end_marker() {
621 if reader
.is_incomplete()?
{
622 println
!("WARNING: file is incomplete");
625 println
!("WARNING: file without end marker");
636 schema
: DRIVE_NAME_SCHEMA
,
640 schema
: OUTPUT_FORMAT
,
646 /// Read Cartridge Memory (Medium auxiliary memory attributes)
647 async
fn cartridge_memory(mut param
: Value
) -> Result
<(), Error
> {
649 let output_format
= extract_output_format(&mut param
);
651 let (config
, _digest
) = pbs_config
::drive
::config()?
;
653 let drive
= extract_drive_name(&mut param
, &config
)?
;
655 let client
= connect_to_localhost()?
;
657 let path
= format
!("api2/json/tape/drive/{}/cartridge-memory", drive
);
658 let mut result
= client
.get(&path
, Some(param
)).await?
;
659 let mut data
= result
["data"].take();
661 let info
= &api2
::tape
::drive
::API_METHOD_CARTRIDGE_MEMORY
;
663 let options
= default_table_format_options()
664 .column(ColumnConfig
::new("id"))
665 .column(ColumnConfig
::new("name"))
666 .column(ColumnConfig
::new("value"))
669 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
677 schema
: DRIVE_NAME_SCHEMA
,
681 schema
: OUTPUT_FORMAT
,
687 /// Read Volume Statistics (SCSI log page 17h)
688 async
fn volume_statistics(mut param
: Value
) -> Result
<(), Error
> {
690 let output_format
= extract_output_format(&mut param
);
692 let (config
, _digest
) = pbs_config
::drive
::config()?
;
694 let drive
= extract_drive_name(&mut param
, &config
)?
;
696 let client
= connect_to_localhost()?
;
698 let path
= format
!("api2/json/tape/drive/{}/volume-statistics", drive
);
699 let mut result
= client
.get(&path
, Some(param
)).await?
;
700 let mut data
= result
["data"].take();
702 let info
= &api2
::tape
::drive
::API_METHOD_VOLUME_STATISTICS
;
704 let options
= default_table_format_options();
706 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
715 schema
: DRIVE_NAME_SCHEMA
,
719 schema
: OUTPUT_FORMAT
,
725 /// Get drive/media status
726 async
fn status(mut param
: Value
) -> Result
<(), Error
> {
728 let output_format
= extract_output_format(&mut param
);
730 let (config
, _digest
) = pbs_config
::drive
::config()?
;
732 let drive
= extract_drive_name(&mut param
, &config
)?
;
734 let client
= connect_to_localhost()?
;
736 let path
= format
!("api2/json/tape/drive/{}/status", drive
);
737 let mut result
= client
.get(&path
, Some(param
)).await?
;
738 let mut data
= result
["data"].take();
740 let info
= &api2
::tape
::drive
::API_METHOD_STATUS
;
742 let render_percentage
= |value
: &Value
, _record
: &Value
| {
743 match value
.as_f64() {
744 Some(wearout
) => Ok(format
!("{:.2}%", wearout
*100.0)),
745 None
=> Ok(String
::from("ERROR")), // should never happen
749 let options
= default_table_format_options()
750 .column(ColumnConfig
::new("blocksize"))
751 .column(ColumnConfig
::new("density"))
752 .column(ColumnConfig
::new("compression"))
753 .column(ColumnConfig
::new("buffer-mode"))
754 .column(ColumnConfig
::new("write-protect"))
755 .column(ColumnConfig
::new("alert-flags"))
756 .column(ColumnConfig
::new("file-number"))
757 .column(ColumnConfig
::new("block-number"))
758 .column(ColumnConfig
::new("manufactured").renderer(render_epoch
))
759 .column(ColumnConfig
::new("bytes-written").renderer(render_bytes_human_readable
))
760 .column(ColumnConfig
::new("bytes-read").renderer(render_bytes_human_readable
))
761 .column(ColumnConfig
::new("medium-passes"))
762 .column(ColumnConfig
::new("medium-wearout").renderer(render_percentage
))
763 .column(ColumnConfig
::new("volume-mounts"))
766 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
775 schema
: DRIVE_NAME_SCHEMA
,
779 schema
: OUTPUT_FORMAT
,
786 async
fn clean_drive(mut param
: Value
) -> Result
<(), Error
> {
788 let output_format
= extract_output_format(&mut param
);
790 let (config
, _digest
) = pbs_config
::drive
::config()?
;
792 let drive
= extract_drive_name(&mut param
, &config
)?
;
794 let mut client
= connect_to_localhost()?
;
796 let path
= format
!("api2/json/tape/drive/{}/clean", drive
);
797 let result
= client
.put(&path
, Some(param
)).await?
;
799 view_task_result(&mut client
, result
, &output_format
).await?
;
808 // Note: We cannot use TapeBackupJobSetup, because drive needs to be optional here
810 // type: TapeBackupJobSetup,
815 schema
: DATASTORE_SCHEMA
,
818 schema
: MEDIA_POOL_NAME_SCHEMA
,
821 schema
: DRIVE_NAME_SCHEMA
,
825 description
: "Eject media upon job completion.",
829 "export-media-set": {
830 description
: "Export media set upon job completion.",
835 description
: "Backup latest snapshots only.",
840 description
: "Ignore the allocation policy and start a new media-set.",
846 schema
: OUTPUT_FORMAT
,
852 /// Backup datastore to tape media pool
853 async
fn backup(mut param
: Value
) -> Result
<(), Error
> {
855 let output_format
= extract_output_format(&mut param
);
857 let (config
, _digest
) = pbs_config
::drive
::config()?
;
859 param
["drive"] = extract_drive_name(&mut param
, &config
)?
.into();
861 let mut client
= connect_to_localhost()?
;
863 let result
= client
.post("api2/json/tape/backup", Some(param
)).await?
;
865 view_task_result(&mut client
, result
, &output_format
).await?
;
874 schema
: DATASTORE_MAP_LIST_SCHEMA
,
877 schema
: DRIVE_NAME_SCHEMA
,
881 description
: "Media set UUID.",
889 description
: "List of snapshots.",
893 schema
: TAPE_RESTORE_SNAPSHOT_SCHEMA
,
901 schema
: OUTPUT_FORMAT
,
907 /// Restore data from media-set
908 async
fn restore(mut param
: Value
) -> Result
<(), Error
> {
910 let output_format
= extract_output_format(&mut param
);
912 let (config
, _digest
) = pbs_config
::drive
::config()?
;
914 param
["drive"] = extract_drive_name(&mut param
, &config
)?
.into();
916 let mut client
= connect_to_localhost()?
;
918 let result
= client
.post("api2/json/tape/restore", Some(param
)).await?
;
920 view_task_result(&mut client
, result
, &output_format
).await?
;
929 schema
: DRIVE_NAME_SCHEMA
,
933 description
: "Force overriding existing index.",
938 description
: "Re-read the whole tape to reconstruct the catalog instead of restoring saved versions.",
943 description
: "Verbose mode - log all found chunks.",
948 schema
: OUTPUT_FORMAT
,
954 /// Scan media and record content
955 async
fn catalog_media(mut param
: Value
) -> Result
<(), Error
> {
957 let output_format
= extract_output_format(&mut param
);
959 let (config
, _digest
) = pbs_config
::drive
::config()?
;
961 let drive
= extract_drive_name(&mut param
, &config
)?
;
963 let mut client
= connect_to_localhost()?
;
965 let path
= format
!("api2/json/tape/drive/{}/catalog", drive
);
966 let result
= client
.post(&path
, Some(param
)).await?
;
968 view_task_result(&mut client
, result
, &output_format
).await?
;
975 let cmd_def
= CliCommandMap
::new()
978 CliCommand
::new(&API_METHOD_BACKUP
)
979 .arg_param(&["store", "pool"])
980 .completion_cb("drive", complete_drive_name
)
981 .completion_cb("store", complete_datastore_name
)
982 .completion_cb("pool", complete_pool_name
)
986 CliCommand
::new(&API_METHOD_RESTORE
)
987 .arg_param(&["media-set", "store", "snapshots"])
988 .completion_cb("store", complete_datastore_name
)
989 .completion_cb("media-set", complete_media_set_uuid
)
990 .completion_cb("snapshots", complete_media_set_snapshots
)
994 CliCommand
::new(&API_METHOD_BARCODE_LABEL_MEDIA
)
995 .completion_cb("drive", complete_drive_name
)
996 .completion_cb("pool", complete_pool_name
)
1000 CliCommand
::new(&API_METHOD_REWIND
)
1001 .completion_cb("drive", complete_drive_name
)
1005 CliCommand
::new(&API_METHOD_DEBUG_SCAN
)
1006 .completion_cb("drive", complete_drive_name
)
1010 CliCommand
::new(&API_METHOD_STATUS
)
1011 .completion_cb("drive", complete_drive_name
)
1015 CliCommand
::new(&API_METHOD_MOVE_TO_EOM
)
1016 .completion_cb("drive", complete_drive_name
)
1020 CliCommand
::new(&API_METHOD_FORMAT_MEDIA
)
1021 .completion_cb("drive", complete_drive_name
)
1025 CliCommand
::new(&API_METHOD_EJECT_MEDIA
)
1026 .completion_cb("drive", complete_drive_name
)
1030 CliCommand
::new(&API_METHOD_INVENTORY
)
1031 .completion_cb("drive", complete_drive_name
)
1035 CliCommand
::new(&API_METHOD_READ_LABEL
)
1036 .completion_cb("drive", complete_drive_name
)
1040 CliCommand
::new(&API_METHOD_CATALOG_MEDIA
)
1041 .completion_cb("drive", complete_drive_name
)
1045 CliCommand
::new(&API_METHOD_CARTRIDGE_MEMORY
)
1046 .completion_cb("drive", complete_drive_name
)
1049 "volume-statistics",
1050 CliCommand
::new(&API_METHOD_VOLUME_STATISTICS
)
1051 .completion_cb("drive", complete_drive_name
)
1055 CliCommand
::new(&API_METHOD_CLEAN_DRIVE
)
1056 .completion_cb("drive", complete_drive_name
)
1060 CliCommand
::new(&API_METHOD_LABEL_MEDIA
)
1061 .completion_cb("drive", complete_drive_name
)
1062 .completion_cb("pool", complete_pool_name
)
1065 .insert("changer", changer_commands())
1066 .insert("drive", drive_commands())
1067 .insert("pool", pool_commands())
1068 .insert("media", media_commands())
1069 .insert("key", encryption_key_commands())
1070 .insert("backup-job", backup_job_commands())
1073 CliCommand
::new(&API_METHOD_LOAD_MEDIA
)
1074 .arg_param(&["label-text"])
1075 .completion_cb("drive", complete_drive_name
)
1076 .completion_cb("label-text", complete_media_label_text
)
1079 "load-media-from-slot",
1080 CliCommand
::new(&API_METHOD_LOAD_MEDIA_FROM_SLOT
)
1081 .arg_param(&["source-slot"])
1082 .completion_cb("drive", complete_drive_name
)
1086 CliCommand
::new(&API_METHOD_UNLOAD_MEDIA
)
1087 .completion_cb("drive", complete_drive_name
)
1091 CliCommand
::new(&API_METHOD_EXPORT_MEDIA
)
1092 .arg_param(&["label-text"])
1093 .completion_cb("drive", complete_drive_name
)
1094 .completion_cb("label-text", complete_media_label_text
)
1098 let mut rpcenv
= CliEnvironment
::new();
1099 rpcenv
.set_auth_id(Some(String
::from("root@pam")));
1101 pbs_runtime
::main(run_async_cli_command(cmd_def
, rpcenv
));