1 use anyhow
::{format_err, Error}
;
2 use serde_json
::{json, Value}
;
9 section_config
::SectionConfigData
,
21 render_bytes_human_readable
,
33 MEDIA_POOL_NAME_SCHEMA
,
38 datastore
::complete_datastore_name
,
39 drive
::complete_drive_name
,
40 media_pool
::complete_pool_name
,
47 complete_media_label_text
,
48 complete_media_set_uuid
,
50 PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0
,
52 proxmox_tape_magic_to_text
,
60 pub fn extract_drive_name(
62 config
: &SectionConfigData
,
63 ) -> Result
<String
, Error
> {
65 let drive
= param
["drive"]
68 .or_else(|| std
::env
::var("PROXMOX_TAPE_DRIVE").ok())
71 let mut drive_names
= Vec
::new();
73 for (name
, (section_type
, _
)) in config
.sections
.iter() {
75 if !(section_type
== "linux" || section_type
== "virtual") { continue; }
76 drive_names
.push(name
);
79 if drive_names
.len() == 1 {
80 Some(drive_names
[0].to_owned())
85 .ok_or_else(|| format_err
!("unable to get (default) drive name"))?
;
87 if let Some(map
) = param
.as_object_mut() {
98 schema
: DRIVE_NAME_SCHEMA
,
102 description
: "Use fast erase.",
108 schema
: OUTPUT_FORMAT
,
115 async
fn erase_media(mut param
: Value
) -> Result
<(), Error
> {
117 let output_format
= get_output_format(¶m
);
119 let (config
, _digest
) = config
::drive
::config()?
;
121 let drive
= extract_drive_name(&mut param
, &config
)?
;
123 let mut client
= connect_to_localhost()?
;
125 let path
= format
!("api2/json/tape/drive/{}/erase-media", drive
);
126 let result
= client
.post(&path
, Some(param
)).await?
;
128 view_task_result(&mut client
, result
, &output_format
).await?
;
137 schema
: DRIVE_NAME_SCHEMA
,
141 schema
: OUTPUT_FORMAT
,
148 async
fn rewind(mut param
: Value
) -> Result
<(), Error
> {
150 let output_format
= get_output_format(¶m
);
152 let (config
, _digest
) = config
::drive
::config()?
;
154 let drive
= extract_drive_name(&mut param
, &config
)?
;
156 let mut client
= connect_to_localhost()?
;
158 let path
= format
!("api2/json/tape/drive/{}/rewind", drive
);
159 let result
= client
.post(&path
, Some(param
)).await?
;
161 view_task_result(&mut client
, result
, &output_format
).await?
;
170 schema
: DRIVE_NAME_SCHEMA
,
174 schema
: OUTPUT_FORMAT
,
180 /// Eject/Unload drive media
181 async
fn eject_media(mut param
: Value
) -> Result
<(), Error
> {
183 let output_format
= get_output_format(¶m
);
185 let (config
, _digest
) = config
::drive
::config()?
;
187 let drive
= extract_drive_name(&mut param
, &config
)?
;
189 let mut client
= connect_to_localhost()?
;
191 let path
= format
!("api2/json/tape/drive/{}/eject-media", drive
);
192 let result
= client
.post(&path
, Some(param
)).await?
;
194 view_task_result(&mut client
, result
, &output_format
).await?
;
203 schema
: DRIVE_NAME_SCHEMA
,
207 schema
: MEDIA_LABEL_SCHEMA
,
210 schema
: OUTPUT_FORMAT
,
216 /// Load media with specified label
217 async
fn load_media(mut param
: Value
) -> Result
<(), Error
> {
219 let output_format
= get_output_format(¶m
);
221 let (config
, _digest
) = config
::drive
::config()?
;
223 let drive
= extract_drive_name(&mut param
, &config
)?
;
225 let mut client
= connect_to_localhost()?
;
227 let path
= format
!("api2/json/tape/drive/{}/load-media", drive
);
228 let result
= client
.post(&path
, Some(param
)).await?
;
230 view_task_result(&mut client
, result
, &output_format
).await?
;
239 schema
: DRIVE_NAME_SCHEMA
,
243 schema
: MEDIA_LABEL_SCHEMA
,
248 /// Export media with specified label
249 async
fn export_media(mut param
: Value
) -> Result
<(), Error
> {
251 let (config
, _digest
) = config
::drive
::config()?
;
253 let drive
= extract_drive_name(&mut param
, &config
)?
;
255 let mut client
= connect_to_localhost()?
;
257 let path
= format
!("api2/json/tape/drive/{}/export-media", drive
);
258 client
.put(&path
, Some(param
)).await?
;
267 schema
: DRIVE_NAME_SCHEMA
,
271 description
: "Source slot number.",
278 /// Load media from the specified slot
279 async
fn load_media_from_slot(mut param
: Value
) -> Result
<(), Error
> {
281 let (config
, _digest
) = config
::drive
::config()?
;
283 let drive
= extract_drive_name(&mut param
, &config
)?
;
285 let mut client
= connect_to_localhost()?
;
287 let path
= format
!("api2/json/tape/drive/{}/load-slot", drive
);
288 client
.put(&path
, Some(param
)).await?
;
297 schema
: DRIVE_NAME_SCHEMA
,
301 description
: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.",
307 schema
: OUTPUT_FORMAT
,
313 /// Unload media via changer
314 async
fn unload_media(mut param
: Value
) -> Result
<(), Error
> {
316 let output_format
= get_output_format(¶m
);
318 let (config
, _digest
) = config
::drive
::config()?
;
320 let drive
= extract_drive_name(&mut param
, &config
)?
;
322 let mut client
= connect_to_localhost()?
;
324 let path
= format
!("api2/json/tape/drive/{}/unload", drive
);
325 let result
= client
.post(&path
, Some(param
)).await?
;
327 view_task_result(&mut client
, result
, &output_format
).await?
;
336 schema
: MEDIA_POOL_NAME_SCHEMA
,
340 schema
: DRIVE_NAME_SCHEMA
,
344 schema
: MEDIA_LABEL_SCHEMA
,
347 schema
: OUTPUT_FORMAT
,
354 async
fn label_media(mut param
: Value
) -> Result
<(), Error
> {
356 let output_format
= get_output_format(¶m
);
358 let (config
, _digest
) = config
::drive
::config()?
;
360 let drive
= extract_drive_name(&mut param
, &config
)?
;
362 let mut client
= connect_to_localhost()?
;
364 let path
= format
!("api2/json/tape/drive/{}/label-media", drive
);
365 let result
= client
.post(&path
, Some(param
)).await?
;
367 view_task_result(&mut client
, result
, &output_format
).await?
;
376 schema
: DRIVE_NAME_SCHEMA
,
380 description
: "Inventorize media",
385 schema
: OUTPUT_FORMAT
,
392 async
fn read_label(mut param
: Value
) -> Result
<(), Error
> {
394 let output_format
= get_output_format(¶m
);
396 let (config
, _digest
) = config
::drive
::config()?
;
398 let drive
= extract_drive_name(&mut param
, &config
)?
;
400 let client
= connect_to_localhost()?
;
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();
406 let info
= &api2
::tape
::drive
::API_METHOD_READ_LABEL
;
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"))
418 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
427 schema
: OUTPUT_FORMAT
,
431 schema
: DRIVE_NAME_SCHEMA
,
435 description
: "Load unknown tapes and try read labels",
440 description
: "Load all tapes and try read labels (even if already inventoried)",
447 /// List (and update) media labels (Changer Inventory)
449 read_labels
: Option
<bool
>,
450 read_all_labels
: Option
<bool
>,
452 ) -> Result
<(), Error
> {
454 let output_format
= get_output_format(¶m
);
456 let (config
, _digest
) = config
::drive
::config()?
;
457 let drive
= extract_drive_name(&mut param
, &config
)?
;
459 let do_read
= read_labels
.unwrap_or(false) || read_all_labels
.unwrap_or(false);
461 let mut client
= connect_to_localhost()?
;
463 let path
= format
!("api2/json/tape/drive/{}/inventory", drive
);
467 let mut param
= json
!({}
);
468 if let Some(true) = read_all_labels
{
469 param
["read-all-labels"] = true.into();
472 let result
= client
.put(&path
, Some(param
)).await?
; // update inventory
473 view_task_result(&mut client
, result
, &output_format
).await?
;
476 let mut result
= client
.get(&path
, None
).await?
;
477 let mut data
= result
["data"].take();
479 let info
= &api2
::tape
::drive
::API_METHOD_INVENTORY
;
481 let options
= default_table_format_options()
482 .column(ColumnConfig
::new("label-text"))
483 .column(ColumnConfig
::new("uuid"))
486 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
495 schema
: MEDIA_POOL_NAME_SCHEMA
,
499 schema
: DRIVE_NAME_SCHEMA
,
503 schema
: OUTPUT_FORMAT
,
509 /// Label media with barcodes from changer device
510 async
fn barcode_label_media(mut param
: Value
) -> Result
<(), Error
> {
512 let output_format
= get_output_format(¶m
);
514 let (config
, _digest
) = config
::drive
::config()?
;
516 let drive
= extract_drive_name(&mut param
, &config
)?
;
518 let mut client
= connect_to_localhost()?
;
520 let path
= format
!("api2/json/tape/drive/{}/barcode-label-media", drive
);
521 let result
= client
.post(&path
, Some(param
)).await?
;
523 view_task_result(&mut client
, result
, &output_format
).await?
;
532 schema
: DRIVE_NAME_SCHEMA
,
538 /// Move to end of media (MTEOM, used to debug)
539 fn move_to_eom(mut param
: Value
) -> Result
<(), Error
> {
541 let (config
, _digest
) = config
::drive
::config()?
;
543 let drive
= extract_drive_name(&mut param
, &config
)?
;
545 let _lock
= lock_tape_device(&config
, &drive
)?
;
547 let mut drive
= open_drive(&config
, &drive
)?
;
549 drive
.move_to_eom()?
;
558 schema
: DRIVE_NAME_SCHEMA
,
564 /// Rewind, then read media contents and print debug info
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
> {
570 let (config
, _digest
) = config
::drive
::config()?
;
572 let drive
= extract_drive_name(&mut param
, &config
)?
;
574 let _lock
= lock_tape_device(&config
, &drive
)?
;
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()?
{
589 Some(mut reader
) => {
590 println
!("got file number {}", file_number
);
592 let header
: Result
<MediaContentHeader
, _
> = unsafe { reader.read_le_value() }
;
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
);
604 println
!("got unknown content header: {:?}", header
.content_magic
);
608 println
!("unable to read content header - {}", err
);
611 let bytes
= reader
.skip_to_end()?
;
612 println
!("skipped {}", HumanByte
::from(bytes
));
622 schema
: DRIVE_NAME_SCHEMA
,
626 schema
: OUTPUT_FORMAT
,
632 /// Read Cartridge Memory (Medium auxiliary memory attributes)
633 async
fn cartridge_memory(mut param
: Value
) -> Result
<(), Error
> {
635 let output_format
= get_output_format(¶m
);
637 let (config
, _digest
) = config
::drive
::config()?
;
639 let drive
= extract_drive_name(&mut param
, &config
)?
;
641 let client
= connect_to_localhost()?
;
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();
647 let info
= &api2
::tape
::drive
::API_METHOD_CARTRIDGE_MEMORY
;
649 let options
= default_table_format_options()
650 .column(ColumnConfig
::new("id"))
651 .column(ColumnConfig
::new("name"))
652 .column(ColumnConfig
::new("value"))
655 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
663 schema
: DRIVE_NAME_SCHEMA
,
667 schema
: OUTPUT_FORMAT
,
673 /// Read Volume Statistics (SCSI log page 17h)
674 async
fn volume_statistics(mut param
: Value
) -> Result
<(), Error
> {
676 let output_format
= get_output_format(¶m
);
678 let (config
, _digest
) = config
::drive
::config()?
;
680 let drive
= extract_drive_name(&mut param
, &config
)?
;
682 let client
= connect_to_localhost()?
;
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();
688 let info
= &api2
::tape
::drive
::API_METHOD_VOLUME_STATISTICS
;
690 let options
= default_table_format_options();
692 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
701 schema
: DRIVE_NAME_SCHEMA
,
705 schema
: OUTPUT_FORMAT
,
711 /// Get drive/media status
712 async
fn status(mut param
: Value
) -> Result
<(), Error
> {
714 let output_format
= get_output_format(¶m
);
716 let (config
, _digest
) = config
::drive
::config()?
;
718 let drive
= extract_drive_name(&mut param
, &config
)?
;
720 let client
= connect_to_localhost()?
;
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();
726 let info
= &api2
::tape
::drive
::API_METHOD_STATUS
;
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
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"))
751 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
760 schema
: DRIVE_NAME_SCHEMA
,
764 schema
: OUTPUT_FORMAT
,
771 async
fn clean_drive(mut param
: Value
) -> Result
<(), Error
> {
773 let output_format
= get_output_format(¶m
);
775 let (config
, _digest
) = config
::drive
::config()?
;
777 let drive
= extract_drive_name(&mut param
, &config
)?
;
779 let mut client
= connect_to_localhost()?
;
781 let path
= format
!("api2/json/tape/drive/{}/clean-drive", drive
);
782 let result
= client
.post(&path
, Some(param
)).await?
;
784 view_task_result(&mut client
, result
, &output_format
).await?
;
793 schema
: DATASTORE_SCHEMA
,
796 schema
: MEDIA_POOL_NAME_SCHEMA
,
799 schema
: DRIVE_NAME_SCHEMA
,
803 description
: "Eject media upon job completion.",
807 "export-media-set": {
808 description
: "Export media set upon job completion.",
813 schema
: OUTPUT_FORMAT
,
819 /// Backup datastore to tape media pool
820 async
fn backup(mut param
: Value
) -> Result
<(), Error
> {
822 let output_format
= get_output_format(¶m
);
824 let (config
, _digest
) = config
::drive
::config()?
;
826 param
["drive"] = extract_drive_name(&mut param
, &config
)?
.into();
828 let mut client
= connect_to_localhost()?
;
830 let result
= client
.post("api2/json/tape/backup", Some(param
)).await?
;
832 view_task_result(&mut client
, result
, &output_format
).await?
;
841 schema
: DATASTORE_SCHEMA
,
844 schema
: DRIVE_NAME_SCHEMA
,
848 description
: "Media set UUID.",
852 schema
: OUTPUT_FORMAT
,
858 /// Restore data from media-set
859 async
fn restore(mut param
: Value
) -> Result
<(), Error
> {
861 let output_format
= get_output_format(¶m
);
863 let (config
, _digest
) = config
::drive
::config()?
;
865 param
["drive"] = extract_drive_name(&mut param
, &config
)?
.into();
867 let mut client
= connect_to_localhost()?
;
869 let result
= client
.post("api2/json/tape/restore", Some(param
)).await?
;
871 view_task_result(&mut client
, result
, &output_format
).await?
;
880 schema
: DRIVE_NAME_SCHEMA
,
884 description
: "Force overriding existing index.",
889 description
: "Verbose mode - log all found chunks.",
894 schema
: OUTPUT_FORMAT
,
900 /// Scan media and record content
901 async
fn catalog_media(mut param
: Value
) -> Result
<(), Error
> {
903 let output_format
= get_output_format(¶m
);
905 let (config
, _digest
) = config
::drive
::config()?
;
907 let drive
= extract_drive_name(&mut param
, &config
)?
;
909 let mut client
= connect_to_localhost()?
;
911 let path
= format
!("api2/json/tape/drive/{}/catalog", drive
);
912 let result
= client
.post(&path
, Some(param
)).await?
;
914 view_task_result(&mut client
, result
, &output_format
).await?
;
921 let cmd_def
= CliCommandMap
::new()
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
)
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
)
938 CliCommand
::new(&API_METHOD_BARCODE_LABEL_MEDIA
)
939 .completion_cb("drive", complete_drive_name
)
940 .completion_cb("pool", complete_pool_name
)
944 CliCommand
::new(&API_METHOD_REWIND
)
945 .completion_cb("drive", complete_drive_name
)
949 CliCommand
::new(&API_METHOD_DEBUG_SCAN
)
950 .completion_cb("drive", complete_drive_name
)
954 CliCommand
::new(&API_METHOD_STATUS
)
955 .completion_cb("drive", complete_drive_name
)
959 CliCommand
::new(&API_METHOD_MOVE_TO_EOM
)
960 .completion_cb("drive", complete_drive_name
)
964 CliCommand
::new(&API_METHOD_ERASE_MEDIA
)
965 .completion_cb("drive", complete_drive_name
)
969 CliCommand
::new(&API_METHOD_EJECT_MEDIA
)
970 .completion_cb("drive", complete_drive_name
)
974 CliCommand
::new(&API_METHOD_INVENTORY
)
975 .completion_cb("drive", complete_drive_name
)
979 CliCommand
::new(&API_METHOD_READ_LABEL
)
980 .completion_cb("drive", complete_drive_name
)
984 CliCommand
::new(&API_METHOD_CATALOG_MEDIA
)
985 .completion_cb("drive", complete_drive_name
)
989 CliCommand
::new(&API_METHOD_CARTRIDGE_MEMORY
)
990 .completion_cb("drive", complete_drive_name
)
994 CliCommand
::new(&API_METHOD_VOLUME_STATISTICS
)
995 .completion_cb("drive", complete_drive_name
)
999 CliCommand
::new(&API_METHOD_CLEAN_DRIVE
)
1000 .completion_cb("drive", complete_drive_name
)
1004 CliCommand
::new(&API_METHOD_LABEL_MEDIA
)
1005 .completion_cb("drive", complete_drive_name
)
1006 .completion_cb("pool", complete_pool_name
)
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())
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
)
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
)
1030 CliCommand
::new(&API_METHOD_UNLOAD_MEDIA
)
1031 .completion_cb("drive", complete_drive_name
)
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
)
1042 let mut rpcenv
= CliEnvironment
::new();
1043 rpcenv
.set_auth_id(Some(String
::from("root@pam")));
1045 proxmox_backup
::tools
::runtime
::main(run_async_cli_command(cmd_def
, rpcenv
));