1 use anyhow
::{format_err, Error}
;
2 use serde_json
::{json, Value}
;
9 section_config
::SectionConfigData
,
21 render_bytes_human_readable
,
32 DATASTORE_MAP_LIST_SCHEMA
,
35 MEDIA_POOL_NAME_SCHEMA
,
41 datastore
::complete_datastore_name
,
42 drive
::complete_drive_name
,
43 media_pool
::complete_pool_name
,
50 set_tape_device_state
,
52 complete_media_label_text
,
53 complete_media_set_uuid
,
55 PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0
,
57 proxmox_tape_magic_to_text
,
65 pub fn extract_drive_name(
67 config
: &SectionConfigData
,
68 ) -> Result
<String
, Error
> {
70 let drive
= param
["drive"]
73 .or_else(|| std
::env
::var("PROXMOX_TAPE_DRIVE").ok())
76 let mut drive_names
= Vec
::new();
78 for (name
, (section_type
, _
)) in config
.sections
.iter() {
80 if !(section_type
== "linux" || section_type
== "virtual") { continue; }
81 drive_names
.push(name
);
84 if drive_names
.len() == 1 {
85 Some(drive_names
[0].to_owned())
90 .ok_or_else(|| format_err
!("unable to get (default) drive name"))?
;
92 if let Some(map
) = param
.as_object_mut() {
103 schema
: DRIVE_NAME_SCHEMA
,
107 description
: "Use fast erase.",
113 schema
: OUTPUT_FORMAT
,
120 async
fn format_media(mut param
: Value
) -> Result
<(), Error
> {
122 let output_format
= get_output_format(¶m
);
124 let (config
, _digest
) = config
::drive
::config()?
;
126 let drive
= extract_drive_name(&mut param
, &config
)?
;
128 let mut client
= connect_to_localhost()?
;
130 let path
= format
!("api2/json/tape/drive/{}/format-media", drive
);
131 let result
= client
.post(&path
, Some(param
)).await?
;
133 view_task_result(&mut client
, result
, &output_format
).await?
;
142 schema
: DRIVE_NAME_SCHEMA
,
146 schema
: OUTPUT_FORMAT
,
153 async
fn rewind(mut param
: Value
) -> Result
<(), Error
> {
155 let output_format
= get_output_format(¶m
);
157 let (config
, _digest
) = config
::drive
::config()?
;
159 let drive
= extract_drive_name(&mut param
, &config
)?
;
161 let mut client
= connect_to_localhost()?
;
163 let path
= format
!("api2/json/tape/drive/{}/rewind", drive
);
164 let result
= client
.post(&path
, Some(param
)).await?
;
166 view_task_result(&mut client
, result
, &output_format
).await?
;
175 schema
: DRIVE_NAME_SCHEMA
,
179 schema
: OUTPUT_FORMAT
,
185 /// Eject/Unload drive media
186 async
fn eject_media(mut param
: Value
) -> Result
<(), Error
> {
188 let output_format
= get_output_format(¶m
);
190 let (config
, _digest
) = config
::drive
::config()?
;
192 let drive
= extract_drive_name(&mut param
, &config
)?
;
194 let mut client
= connect_to_localhost()?
;
196 let path
= format
!("api2/json/tape/drive/{}/eject-media", drive
);
197 let result
= client
.post(&path
, Some(param
)).await?
;
199 view_task_result(&mut client
, result
, &output_format
).await?
;
208 schema
: DRIVE_NAME_SCHEMA
,
212 schema
: MEDIA_LABEL_SCHEMA
,
215 schema
: OUTPUT_FORMAT
,
221 /// Load media with specified label
222 async
fn load_media(mut param
: Value
) -> Result
<(), Error
> {
224 let output_format
= get_output_format(¶m
);
226 let (config
, _digest
) = config
::drive
::config()?
;
228 let drive
= extract_drive_name(&mut param
, &config
)?
;
230 let mut client
= connect_to_localhost()?
;
232 let path
= format
!("api2/json/tape/drive/{}/load-media", drive
);
233 let result
= client
.post(&path
, Some(param
)).await?
;
235 view_task_result(&mut client
, result
, &output_format
).await?
;
244 schema
: DRIVE_NAME_SCHEMA
,
248 schema
: MEDIA_LABEL_SCHEMA
,
253 /// Export media with specified label
254 async
fn export_media(mut param
: Value
) -> Result
<(), Error
> {
256 let (config
, _digest
) = config
::drive
::config()?
;
258 let drive
= extract_drive_name(&mut param
, &config
)?
;
260 let mut client
= connect_to_localhost()?
;
262 let path
= format
!("api2/json/tape/drive/{}/export-media", drive
);
263 client
.put(&path
, Some(param
)).await?
;
272 schema
: DRIVE_NAME_SCHEMA
,
276 description
: "Source slot number.",
283 /// Load media from the specified slot
284 async
fn load_media_from_slot(mut param
: Value
) -> Result
<(), Error
> {
286 let (config
, _digest
) = config
::drive
::config()?
;
288 let drive
= extract_drive_name(&mut param
, &config
)?
;
290 let mut client
= connect_to_localhost()?
;
292 let path
= format
!("api2/json/tape/drive/{}/load-slot", drive
);
293 client
.put(&path
, Some(param
)).await?
;
302 schema
: DRIVE_NAME_SCHEMA
,
306 description
: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.",
312 schema
: OUTPUT_FORMAT
,
318 /// Unload media via changer
319 async
fn unload_media(mut param
: Value
) -> Result
<(), Error
> {
321 let output_format
= get_output_format(¶m
);
323 let (config
, _digest
) = config
::drive
::config()?
;
325 let drive
= extract_drive_name(&mut param
, &config
)?
;
327 let mut client
= connect_to_localhost()?
;
329 let path
= format
!("api2/json/tape/drive/{}/unload", drive
);
330 let result
= client
.post(&path
, Some(param
)).await?
;
332 view_task_result(&mut client
, result
, &output_format
).await?
;
341 schema
: MEDIA_POOL_NAME_SCHEMA
,
345 schema
: DRIVE_NAME_SCHEMA
,
349 schema
: MEDIA_LABEL_SCHEMA
,
352 schema
: OUTPUT_FORMAT
,
359 async
fn label_media(mut param
: Value
) -> Result
<(), Error
> {
361 let output_format
= get_output_format(¶m
);
363 let (config
, _digest
) = config
::drive
::config()?
;
365 let drive
= extract_drive_name(&mut param
, &config
)?
;
367 let mut client
= connect_to_localhost()?
;
369 let path
= format
!("api2/json/tape/drive/{}/label-media", drive
);
370 let result
= client
.post(&path
, Some(param
)).await?
;
372 view_task_result(&mut client
, result
, &output_format
).await?
;
381 schema
: DRIVE_NAME_SCHEMA
,
385 description
: "Inventorize media",
390 schema
: OUTPUT_FORMAT
,
397 async
fn read_label(mut param
: Value
) -> Result
<(), Error
> {
399 let output_format
= get_output_format(¶m
);
401 let (config
, _digest
) = config
::drive
::config()?
;
403 let drive
= extract_drive_name(&mut param
, &config
)?
;
405 let client
= connect_to_localhost()?
;
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();
411 let info
= &api2
::tape
::drive
::API_METHOD_READ_LABEL
;
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"))
423 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
432 schema
: OUTPUT_FORMAT
,
436 schema
: DRIVE_NAME_SCHEMA
,
440 description
: "Load unknown tapes and try read labels",
445 description
: "Load all tapes and try read labels (even if already inventoried)",
452 /// List (and update) media labels (Changer Inventory)
454 read_labels
: Option
<bool
>,
455 read_all_labels
: Option
<bool
>,
457 ) -> Result
<(), Error
> {
459 let output_format
= get_output_format(¶m
);
461 let (config
, _digest
) = config
::drive
::config()?
;
462 let drive
= extract_drive_name(&mut param
, &config
)?
;
464 let do_read
= read_labels
.unwrap_or(false) || read_all_labels
.unwrap_or(false);
466 let mut client
= connect_to_localhost()?
;
468 let path
= format
!("api2/json/tape/drive/{}/inventory", drive
);
472 let mut param
= json
!({}
);
473 if let Some(true) = read_all_labels
{
474 param
["read-all-labels"] = true.into();
477 let result
= client
.put(&path
, Some(param
)).await?
; // update inventory
478 view_task_result(&mut client
, result
, &output_format
).await?
;
481 let mut result
= client
.get(&path
, None
).await?
;
482 let mut data
= result
["data"].take();
484 let info
= &api2
::tape
::drive
::API_METHOD_INVENTORY
;
486 let options
= default_table_format_options()
487 .column(ColumnConfig
::new("label-text"))
488 .column(ColumnConfig
::new("uuid"))
491 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
500 schema
: MEDIA_POOL_NAME_SCHEMA
,
504 schema
: DRIVE_NAME_SCHEMA
,
508 schema
: OUTPUT_FORMAT
,
514 /// Label media with barcodes from changer device
515 async
fn barcode_label_media(mut param
: Value
) -> Result
<(), Error
> {
517 let output_format
= get_output_format(¶m
);
519 let (config
, _digest
) = config
::drive
::config()?
;
521 let drive
= extract_drive_name(&mut param
, &config
)?
;
523 let mut client
= connect_to_localhost()?
;
525 let path
= format
!("api2/json/tape/drive/{}/barcode-label-media", drive
);
526 let result
= client
.post(&path
, Some(param
)).await?
;
528 view_task_result(&mut client
, result
, &output_format
).await?
;
537 schema
: DRIVE_NAME_SCHEMA
,
543 /// Move to end of media (MTEOM, used to debug)
544 fn move_to_eom(mut param
: Value
) -> Result
<(), Error
> {
546 let (config
, _digest
) = config
::drive
::config()?
;
548 let drive
= extract_drive_name(&mut param
, &config
)?
;
550 let _lock
= lock_tape_device(&config
, &drive
)?
;
551 set_tape_device_state(&drive
, "moving to eom")?
;
553 let mut drive
= open_drive(&config
, &drive
)?
;
555 drive
.move_to_eom(false)?
;
564 schema
: DRIVE_NAME_SCHEMA
,
570 /// Rewind, then read media contents and print debug info
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
> {
576 let (config
, _digest
) = config
::drive
::config()?
;
578 let drive
= extract_drive_name(&mut param
, &config
)?
;
580 let _lock
= lock_tape_device(&config
, &drive
)?
;
581 set_tape_device_state(&drive
, "debug scan")?
;
583 let mut drive
= open_drive(&config
, &drive
)?
;
585 println
!("rewinding tape");
589 let file_number
= drive
.current_file_number()?
;
591 match drive
.read_next_file() {
592 Err(BlockReadError
::EndOfFile
) => {
593 println
!("filemark number {}", file_number
);
596 Err(BlockReadError
::EndOfStream
) => {
600 Err(BlockReadError
::Error(err
)) => {
601 return Err(err
.into());
604 println
!("got file number {}", file_number
);
606 let header
: Result
<MediaContentHeader
, _
> = unsafe { reader.read_le_value() }
;
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
);
618 println
!("got unknown content header: {:?}", header
.content_magic
);
622 println
!("unable to read content header - {}", err
);
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");
632 println
!("WARNING: file without end marker");
643 schema
: DRIVE_NAME_SCHEMA
,
647 schema
: OUTPUT_FORMAT
,
653 /// Read Cartridge Memory (Medium auxiliary memory attributes)
654 async
fn cartridge_memory(mut param
: Value
) -> Result
<(), Error
> {
656 let output_format
= get_output_format(¶m
);
658 let (config
, _digest
) = config
::drive
::config()?
;
660 let drive
= extract_drive_name(&mut param
, &config
)?
;
662 let client
= connect_to_localhost()?
;
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();
668 let info
= &api2
::tape
::drive
::API_METHOD_CARTRIDGE_MEMORY
;
670 let options
= default_table_format_options()
671 .column(ColumnConfig
::new("id"))
672 .column(ColumnConfig
::new("name"))
673 .column(ColumnConfig
::new("value"))
676 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
684 schema
: DRIVE_NAME_SCHEMA
,
688 schema
: OUTPUT_FORMAT
,
694 /// Read Volume Statistics (SCSI log page 17h)
695 async
fn volume_statistics(mut param
: Value
) -> Result
<(), Error
> {
697 let output_format
= get_output_format(¶m
);
699 let (config
, _digest
) = config
::drive
::config()?
;
701 let drive
= extract_drive_name(&mut param
, &config
)?
;
703 let client
= connect_to_localhost()?
;
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();
709 let info
= &api2
::tape
::drive
::API_METHOD_VOLUME_STATISTICS
;
711 let options
= default_table_format_options();
713 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
722 schema
: DRIVE_NAME_SCHEMA
,
726 schema
: OUTPUT_FORMAT
,
732 /// Get drive/media status
733 async
fn status(mut param
: Value
) -> Result
<(), Error
> {
735 let output_format
= get_output_format(¶m
);
737 let (config
, _digest
) = config
::drive
::config()?
;
739 let drive
= extract_drive_name(&mut param
, &config
)?
;
741 let client
= connect_to_localhost()?
;
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();
747 let info
= &api2
::tape
::drive
::API_METHOD_STATUS
;
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
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"))
773 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
782 schema
: DRIVE_NAME_SCHEMA
,
786 schema
: OUTPUT_FORMAT
,
793 async
fn clean_drive(mut param
: Value
) -> Result
<(), Error
> {
795 let output_format
= get_output_format(¶m
);
797 let (config
, _digest
) = config
::drive
::config()?
;
799 let drive
= extract_drive_name(&mut param
, &config
)?
;
801 let mut client
= connect_to_localhost()?
;
803 let path
= format
!("api2/json/tape/drive/{}/clean", drive
);
804 let result
= client
.put(&path
, Some(param
)).await?
;
806 view_task_result(&mut client
, result
, &output_format
).await?
;
815 // Note: We cannot use TapeBackupJobSetup, because drive needs to be optional here
817 // type: TapeBackupJobSetup,
822 schema
: DATASTORE_SCHEMA
,
825 schema
: MEDIA_POOL_NAME_SCHEMA
,
828 schema
: DRIVE_NAME_SCHEMA
,
832 description
: "Eject media upon job completion.",
836 "export-media-set": {
837 description
: "Export media set upon job completion.",
842 description
: "Backup latest snapshots only.",
847 schema
: OUTPUT_FORMAT
,
853 /// Backup datastore to tape media pool
854 async
fn backup(mut param
: Value
) -> Result
<(), Error
> {
856 let output_format
= get_output_format(¶m
);
858 let (config
, _digest
) = config
::drive
::config()?
;
860 param
["drive"] = extract_drive_name(&mut param
, &config
)?
.into();
862 let mut client
= connect_to_localhost()?
;
864 let result
= client
.post("api2/json/tape/backup", Some(param
)).await?
;
866 view_task_result(&mut client
, result
, &output_format
).await?
;
875 schema
: DATASTORE_MAP_LIST_SCHEMA
,
878 schema
: DRIVE_NAME_SCHEMA
,
882 description
: "Media set UUID.",
894 schema
: OUTPUT_FORMAT
,
900 /// Restore data from media-set
901 async
fn restore(mut param
: Value
) -> Result
<(), Error
> {
903 let output_format
= get_output_format(¶m
);
905 let (config
, _digest
) = config
::drive
::config()?
;
907 param
["drive"] = extract_drive_name(&mut param
, &config
)?
.into();
909 let mut client
= connect_to_localhost()?
;
911 let result
= client
.post("api2/json/tape/restore", Some(param
)).await?
;
913 view_task_result(&mut client
, result
, &output_format
).await?
;
922 schema
: DRIVE_NAME_SCHEMA
,
926 description
: "Force overriding existing index.",
931 description
: "Re-read the whole tape to reconstruct the catalog instead of restoring saved versions.",
936 description
: "Verbose mode - log all found chunks.",
941 schema
: OUTPUT_FORMAT
,
947 /// Scan media and record content
948 async
fn catalog_media(mut param
: Value
) -> Result
<(), Error
> {
950 let output_format
= get_output_format(¶m
);
952 let (config
, _digest
) = config
::drive
::config()?
;
954 let drive
= extract_drive_name(&mut param
, &config
)?
;
956 let mut client
= connect_to_localhost()?
;
958 let path
= format
!("api2/json/tape/drive/{}/catalog", drive
);
959 let result
= client
.post(&path
, Some(param
)).await?
;
961 view_task_result(&mut client
, result
, &output_format
).await?
;
968 let cmd_def
= CliCommandMap
::new()
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
)
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
)
986 CliCommand
::new(&API_METHOD_BARCODE_LABEL_MEDIA
)
987 .completion_cb("drive", complete_drive_name
)
988 .completion_cb("pool", complete_pool_name
)
992 CliCommand
::new(&API_METHOD_REWIND
)
993 .completion_cb("drive", complete_drive_name
)
997 CliCommand
::new(&API_METHOD_DEBUG_SCAN
)
998 .completion_cb("drive", complete_drive_name
)
1002 CliCommand
::new(&API_METHOD_STATUS
)
1003 .completion_cb("drive", complete_drive_name
)
1007 CliCommand
::new(&API_METHOD_MOVE_TO_EOM
)
1008 .completion_cb("drive", complete_drive_name
)
1012 CliCommand
::new(&API_METHOD_FORMAT_MEDIA
)
1013 .completion_cb("drive", complete_drive_name
)
1017 CliCommand
::new(&API_METHOD_EJECT_MEDIA
)
1018 .completion_cb("drive", complete_drive_name
)
1022 CliCommand
::new(&API_METHOD_INVENTORY
)
1023 .completion_cb("drive", complete_drive_name
)
1027 CliCommand
::new(&API_METHOD_READ_LABEL
)
1028 .completion_cb("drive", complete_drive_name
)
1032 CliCommand
::new(&API_METHOD_CATALOG_MEDIA
)
1033 .completion_cb("drive", complete_drive_name
)
1037 CliCommand
::new(&API_METHOD_CARTRIDGE_MEMORY
)
1038 .completion_cb("drive", complete_drive_name
)
1041 "volume-statistics",
1042 CliCommand
::new(&API_METHOD_VOLUME_STATISTICS
)
1043 .completion_cb("drive", complete_drive_name
)
1047 CliCommand
::new(&API_METHOD_CLEAN_DRIVE
)
1048 .completion_cb("drive", complete_drive_name
)
1052 CliCommand
::new(&API_METHOD_LABEL_MEDIA
)
1053 .completion_cb("drive", complete_drive_name
)
1054 .completion_cb("pool", complete_pool_name
)
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())
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
)
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
)
1078 CliCommand
::new(&API_METHOD_UNLOAD_MEDIA
)
1079 .completion_cb("drive", complete_drive_name
)
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
)
1090 let mut rpcenv
= CliEnvironment
::new();
1091 rpcenv
.set_auth_id(Some(String
::from("root@pam")));
1093 proxmox_backup
::tools
::runtime
::main(run_async_cli_command(cmd_def
, rpcenv
));