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
,
37 TAPE_RESTORE_SNAPSHOT_SCHEMA
,
42 datastore
::complete_datastore_name
,
43 drive
::complete_drive_name
,
44 media_pool
::complete_pool_name
,
51 set_tape_device_state
,
53 complete_media_label_text
,
54 complete_media_set_uuid
,
55 complete_media_set_snapshots
,
57 PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0
,
59 proxmox_tape_magic_to_text
,
67 pub fn extract_drive_name(
69 config
: &SectionConfigData
,
70 ) -> Result
<String
, Error
> {
72 let drive
= param
["drive"]
75 .or_else(|| std
::env
::var("PROXMOX_TAPE_DRIVE").ok())
78 let mut drive_names
= Vec
::new();
80 for (name
, (section_type
, _
)) in config
.sections
.iter() {
82 if !(section_type
== "linux" || section_type
== "virtual") { continue; }
83 drive_names
.push(name
);
86 if drive_names
.len() == 1 {
87 Some(drive_names
[0].to_owned())
92 .ok_or_else(|| format_err
!("unable to get (default) drive name"))?
;
94 if let Some(map
) = param
.as_object_mut() {
105 schema
: DRIVE_NAME_SCHEMA
,
109 description
: "Use fast erase.",
115 schema
: OUTPUT_FORMAT
,
122 async
fn format_media(mut param
: Value
) -> Result
<(), Error
> {
124 let output_format
= extract_output_format(&mut param
);
126 let (config
, _digest
) = config
::drive
::config()?
;
128 let drive
= extract_drive_name(&mut param
, &config
)?
;
130 let mut client
= connect_to_localhost()?
;
132 let path
= format
!("api2/json/tape/drive/{}/format-media", drive
);
133 let result
= client
.post(&path
, Some(param
)).await?
;
135 view_task_result(&mut client
, result
, &output_format
).await?
;
144 schema
: DRIVE_NAME_SCHEMA
,
148 schema
: OUTPUT_FORMAT
,
155 async
fn rewind(mut param
: Value
) -> Result
<(), Error
> {
157 let output_format
= extract_output_format(&mut param
);
159 let (config
, _digest
) = config
::drive
::config()?
;
161 let drive
= extract_drive_name(&mut param
, &config
)?
;
163 let mut client
= connect_to_localhost()?
;
165 let path
= format
!("api2/json/tape/drive/{}/rewind", drive
);
166 let result
= client
.post(&path
, Some(param
)).await?
;
168 view_task_result(&mut client
, result
, &output_format
).await?
;
177 schema
: DRIVE_NAME_SCHEMA
,
181 schema
: OUTPUT_FORMAT
,
187 /// Eject/Unload drive media
188 async
fn eject_media(mut param
: Value
) -> Result
<(), Error
> {
190 let output_format
= extract_output_format(&mut param
);
192 let (config
, _digest
) = config
::drive
::config()?
;
194 let drive
= extract_drive_name(&mut param
, &config
)?
;
196 let mut client
= connect_to_localhost()?
;
198 let path
= format
!("api2/json/tape/drive/{}/eject-media", drive
);
199 let result
= client
.post(&path
, Some(param
)).await?
;
201 view_task_result(&mut client
, result
, &output_format
).await?
;
210 schema
: DRIVE_NAME_SCHEMA
,
214 schema
: MEDIA_LABEL_SCHEMA
,
217 schema
: OUTPUT_FORMAT
,
223 /// Load media with specified label
224 async
fn load_media(mut param
: Value
) -> Result
<(), Error
> {
226 let output_format
= extract_output_format(&mut param
);
228 let (config
, _digest
) = config
::drive
::config()?
;
230 let drive
= extract_drive_name(&mut param
, &config
)?
;
232 let mut client
= connect_to_localhost()?
;
234 let path
= format
!("api2/json/tape/drive/{}/load-media", drive
);
235 let result
= client
.post(&path
, Some(param
)).await?
;
237 view_task_result(&mut client
, result
, &output_format
).await?
;
246 schema
: DRIVE_NAME_SCHEMA
,
250 schema
: MEDIA_LABEL_SCHEMA
,
255 /// Export media with specified label
256 async
fn export_media(mut param
: Value
) -> Result
<(), Error
> {
258 let (config
, _digest
) = config
::drive
::config()?
;
260 let drive
= extract_drive_name(&mut param
, &config
)?
;
262 let mut client
= connect_to_localhost()?
;
264 let path
= format
!("api2/json/tape/drive/{}/export-media", drive
);
265 client
.put(&path
, Some(param
)).await?
;
274 schema
: DRIVE_NAME_SCHEMA
,
278 description
: "Source slot number.",
285 /// Load media from the specified slot
286 async
fn load_media_from_slot(mut param
: Value
) -> Result
<(), Error
> {
288 let (config
, _digest
) = config
::drive
::config()?
;
290 let drive
= extract_drive_name(&mut param
, &config
)?
;
292 let mut client
= connect_to_localhost()?
;
294 let path
= format
!("api2/json/tape/drive/{}/load-slot", drive
);
295 client
.put(&path
, Some(param
)).await?
;
304 schema
: DRIVE_NAME_SCHEMA
,
308 description
: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.",
314 schema
: OUTPUT_FORMAT
,
320 /// Unload media via changer
321 async
fn unload_media(mut param
: Value
) -> Result
<(), Error
> {
323 let output_format
= extract_output_format(&mut param
);
325 let (config
, _digest
) = config
::drive
::config()?
;
327 let drive
= extract_drive_name(&mut param
, &config
)?
;
329 let mut client
= connect_to_localhost()?
;
331 let path
= format
!("api2/json/tape/drive/{}/unload", drive
);
332 let result
= client
.post(&path
, Some(param
)).await?
;
334 view_task_result(&mut client
, result
, &output_format
).await?
;
343 schema
: MEDIA_POOL_NAME_SCHEMA
,
347 schema
: DRIVE_NAME_SCHEMA
,
351 schema
: MEDIA_LABEL_SCHEMA
,
354 schema
: OUTPUT_FORMAT
,
361 async
fn label_media(mut param
: Value
) -> Result
<(), Error
> {
363 let output_format
= extract_output_format(&mut param
);
365 let (config
, _digest
) = config
::drive
::config()?
;
367 let drive
= extract_drive_name(&mut param
, &config
)?
;
369 let mut client
= connect_to_localhost()?
;
371 let path
= format
!("api2/json/tape/drive/{}/label-media", drive
);
372 let result
= client
.post(&path
, Some(param
)).await?
;
374 view_task_result(&mut client
, result
, &output_format
).await?
;
383 schema
: DRIVE_NAME_SCHEMA
,
387 description
: "Inventorize media",
392 schema
: OUTPUT_FORMAT
,
399 async
fn read_label(mut param
: Value
) -> Result
<(), Error
> {
401 let output_format
= extract_output_format(&mut param
);
403 let (config
, _digest
) = config
::drive
::config()?
;
405 let drive
= extract_drive_name(&mut param
, &config
)?
;
407 let client
= connect_to_localhost()?
;
409 let path
= format
!("api2/json/tape/drive/{}/read-label", drive
);
410 let mut result
= client
.get(&path
, Some(param
)).await?
;
411 let mut data
= result
["data"].take();
413 let info
= &api2
::tape
::drive
::API_METHOD_READ_LABEL
;
415 let options
= default_table_format_options()
416 .column(ColumnConfig
::new("label-text"))
417 .column(ColumnConfig
::new("uuid"))
418 .column(ColumnConfig
::new("ctime").renderer(render_epoch
))
419 .column(ColumnConfig
::new("pool"))
420 .column(ColumnConfig
::new("media-set-uuid"))
421 .column(ColumnConfig
::new("media-set-ctime").renderer(render_epoch
))
422 .column(ColumnConfig
::new("encryption-key-fingerprint"))
425 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
434 schema
: OUTPUT_FORMAT
,
438 schema
: DRIVE_NAME_SCHEMA
,
442 description
: "Load unknown tapes and try read labels",
447 description
: "Load all tapes and try read labels (even if already inventoried)",
454 /// List (and update) media labels (Changer Inventory)
456 read_labels
: Option
<bool
>,
457 read_all_labels
: Option
<bool
>,
459 ) -> Result
<(), Error
> {
461 let output_format
= extract_output_format(&mut param
);
463 let (config
, _digest
) = config
::drive
::config()?
;
464 let drive
= extract_drive_name(&mut param
, &config
)?
;
466 let do_read
= read_labels
.unwrap_or(false) || read_all_labels
.unwrap_or(false);
468 let mut client
= connect_to_localhost()?
;
470 let path
= format
!("api2/json/tape/drive/{}/inventory", drive
);
474 let mut param
= json
!({}
);
475 if let Some(true) = read_all_labels
{
476 param
["read-all-labels"] = true.into();
479 let result
= client
.put(&path
, Some(param
)).await?
; // update inventory
480 view_task_result(&mut client
, result
, &output_format
).await?
;
483 let mut result
= client
.get(&path
, None
).await?
;
484 let mut data
= result
["data"].take();
486 let info
= &api2
::tape
::drive
::API_METHOD_INVENTORY
;
488 let options
= default_table_format_options()
489 .column(ColumnConfig
::new("label-text"))
490 .column(ColumnConfig
::new("uuid"))
493 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
502 schema
: MEDIA_POOL_NAME_SCHEMA
,
506 schema
: DRIVE_NAME_SCHEMA
,
510 schema
: OUTPUT_FORMAT
,
516 /// Label media with barcodes from changer device
517 async
fn barcode_label_media(mut param
: Value
) -> Result
<(), Error
> {
519 let output_format
= extract_output_format(&mut param
);
521 let (config
, _digest
) = config
::drive
::config()?
;
523 let drive
= extract_drive_name(&mut param
, &config
)?
;
525 let mut client
= connect_to_localhost()?
;
527 let path
= format
!("api2/json/tape/drive/{}/barcode-label-media", drive
);
528 let result
= client
.post(&path
, Some(param
)).await?
;
530 view_task_result(&mut client
, result
, &output_format
).await?
;
539 schema
: DRIVE_NAME_SCHEMA
,
545 /// Move to end of media (MTEOM, used to debug)
546 fn move_to_eom(mut param
: Value
) -> Result
<(), Error
> {
548 let (config
, _digest
) = config
::drive
::config()?
;
550 let drive
= extract_drive_name(&mut param
, &config
)?
;
552 let _lock
= lock_tape_device(&config
, &drive
)?
;
553 set_tape_device_state(&drive
, "moving to eom")?
;
555 let mut drive
= open_drive(&config
, &drive
)?
;
557 drive
.move_to_eom(false)?
;
566 schema
: DRIVE_NAME_SCHEMA
,
572 /// Rewind, then read media contents and print debug info
574 /// Note: This reads unless the driver returns an IO Error, so this
575 /// method is expected to fails when we reach EOT.
576 fn debug_scan(mut param
: Value
) -> Result
<(), Error
> {
578 let (config
, _digest
) = config
::drive
::config()?
;
580 let drive
= extract_drive_name(&mut param
, &config
)?
;
582 let _lock
= lock_tape_device(&config
, &drive
)?
;
583 set_tape_device_state(&drive
, "debug scan")?
;
585 let mut drive
= open_drive(&config
, &drive
)?
;
587 println
!("rewinding tape");
591 let file_number
= drive
.current_file_number()?
;
593 match drive
.read_next_file() {
594 Err(BlockReadError
::EndOfFile
) => {
595 println
!("filemark number {}", file_number
);
598 Err(BlockReadError
::EndOfStream
) => {
602 Err(BlockReadError
::Error(err
)) => {
603 return Err(err
.into());
606 println
!("got file number {}", file_number
);
608 let header
: Result
<MediaContentHeader
, _
> = unsafe { reader.read_le_value() }
;
611 if header
.magic
!= PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0
{
612 println
!("got MediaContentHeader with wrong magic: {:?}", header
.magic
);
613 } else if let Some(name
) = proxmox_tape_magic_to_text(&header
.content_magic
) {
614 println
!("got content header: {}", name
);
615 println
!(" uuid: {}", header
.content_uuid());
616 println
!(" ctime: {}", strftime_local("%c", header
.ctime
)?
);
617 println
!(" hsize: {}", HumanByte
::from(header
.size
as usize));
618 println
!(" part: {}", header
.part_number
);
620 println
!("got unknown content header: {:?}", header
.content_magic
);
624 println
!("unable to read content header - {}", err
);
627 let bytes
= reader
.skip_data()?
;
628 println
!("skipped {}", HumanByte
::from(bytes
));
629 if let Ok(true) = reader
.has_end_marker() {
630 if reader
.is_incomplete()?
{
631 println
!("WARNING: file is incomplete");
634 println
!("WARNING: file without end marker");
645 schema
: DRIVE_NAME_SCHEMA
,
649 schema
: OUTPUT_FORMAT
,
655 /// Read Cartridge Memory (Medium auxiliary memory attributes)
656 async
fn cartridge_memory(mut param
: Value
) -> Result
<(), Error
> {
658 let output_format
= extract_output_format(&mut param
);
660 let (config
, _digest
) = config
::drive
::config()?
;
662 let drive
= extract_drive_name(&mut param
, &config
)?
;
664 let client
= connect_to_localhost()?
;
666 let path
= format
!("api2/json/tape/drive/{}/cartridge-memory", drive
);
667 let mut result
= client
.get(&path
, Some(param
)).await?
;
668 let mut data
= result
["data"].take();
670 let info
= &api2
::tape
::drive
::API_METHOD_CARTRIDGE_MEMORY
;
672 let options
= default_table_format_options()
673 .column(ColumnConfig
::new("id"))
674 .column(ColumnConfig
::new("name"))
675 .column(ColumnConfig
::new("value"))
678 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
686 schema
: DRIVE_NAME_SCHEMA
,
690 schema
: OUTPUT_FORMAT
,
696 /// Read Volume Statistics (SCSI log page 17h)
697 async
fn volume_statistics(mut param
: Value
) -> Result
<(), Error
> {
699 let output_format
= extract_output_format(&mut param
);
701 let (config
, _digest
) = config
::drive
::config()?
;
703 let drive
= extract_drive_name(&mut param
, &config
)?
;
705 let client
= connect_to_localhost()?
;
707 let path
= format
!("api2/json/tape/drive/{}/volume-statistics", drive
);
708 let mut result
= client
.get(&path
, Some(param
)).await?
;
709 let mut data
= result
["data"].take();
711 let info
= &api2
::tape
::drive
::API_METHOD_VOLUME_STATISTICS
;
713 let options
= default_table_format_options();
715 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
724 schema
: DRIVE_NAME_SCHEMA
,
728 schema
: OUTPUT_FORMAT
,
734 /// Get drive/media status
735 async
fn status(mut param
: Value
) -> Result
<(), Error
> {
737 let output_format
= extract_output_format(&mut param
);
739 let (config
, _digest
) = config
::drive
::config()?
;
741 let drive
= extract_drive_name(&mut param
, &config
)?
;
743 let client
= connect_to_localhost()?
;
745 let path
= format
!("api2/json/tape/drive/{}/status", drive
);
746 let mut result
= client
.get(&path
, Some(param
)).await?
;
747 let mut data
= result
["data"].take();
749 let info
= &api2
::tape
::drive
::API_METHOD_STATUS
;
751 let render_percentage
= |value
: &Value
, _record
: &Value
| {
752 match value
.as_f64() {
753 Some(wearout
) => Ok(format
!("{:.2}%", wearout
*100.0)),
754 None
=> Ok(String
::from("ERROR")), // should never happen
758 let options
= default_table_format_options()
759 .column(ColumnConfig
::new("blocksize"))
760 .column(ColumnConfig
::new("density"))
761 .column(ColumnConfig
::new("compression"))
762 .column(ColumnConfig
::new("buffer-mode"))
763 .column(ColumnConfig
::new("write-protect"))
764 .column(ColumnConfig
::new("alert-flags"))
765 .column(ColumnConfig
::new("file-number"))
766 .column(ColumnConfig
::new("block-number"))
767 .column(ColumnConfig
::new("manufactured").renderer(render_epoch
))
768 .column(ColumnConfig
::new("bytes-written").renderer(render_bytes_human_readable
))
769 .column(ColumnConfig
::new("bytes-read").renderer(render_bytes_human_readable
))
770 .column(ColumnConfig
::new("medium-passes"))
771 .column(ColumnConfig
::new("medium-wearout").renderer(render_percentage
))
772 .column(ColumnConfig
::new("volume-mounts"))
775 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
784 schema
: DRIVE_NAME_SCHEMA
,
788 schema
: OUTPUT_FORMAT
,
795 async
fn clean_drive(mut param
: Value
) -> Result
<(), Error
> {
797 let output_format
= extract_output_format(&mut param
);
799 let (config
, _digest
) = config
::drive
::config()?
;
801 let drive
= extract_drive_name(&mut param
, &config
)?
;
803 let mut client
= connect_to_localhost()?
;
805 let path
= format
!("api2/json/tape/drive/{}/clean", drive
);
806 let result
= client
.put(&path
, Some(param
)).await?
;
808 view_task_result(&mut client
, result
, &output_format
).await?
;
817 // Note: We cannot use TapeBackupJobSetup, because drive needs to be optional here
819 // type: TapeBackupJobSetup,
824 schema
: DATASTORE_SCHEMA
,
827 schema
: MEDIA_POOL_NAME_SCHEMA
,
830 schema
: DRIVE_NAME_SCHEMA
,
834 description
: "Eject media upon job completion.",
838 "export-media-set": {
839 description
: "Export media set upon job completion.",
844 description
: "Backup latest snapshots only.",
849 schema
: OUTPUT_FORMAT
,
855 /// Backup datastore to tape media pool
856 async
fn backup(mut param
: Value
) -> Result
<(), Error
> {
858 let output_format
= extract_output_format(&mut param
);
860 let (config
, _digest
) = config
::drive
::config()?
;
862 param
["drive"] = extract_drive_name(&mut param
, &config
)?
.into();
864 let mut client
= connect_to_localhost()?
;
866 let result
= client
.post("api2/json/tape/backup", Some(param
)).await?
;
868 view_task_result(&mut client
, result
, &output_format
).await?
;
877 schema
: DATASTORE_MAP_LIST_SCHEMA
,
880 schema
: DRIVE_NAME_SCHEMA
,
884 description
: "Media set UUID.",
892 description
: "List of snapshots.",
896 schema
: TAPE_RESTORE_SNAPSHOT_SCHEMA
,
904 schema
: OUTPUT_FORMAT
,
910 /// Restore data from media-set
911 async
fn restore(mut param
: Value
) -> Result
<(), Error
> {
913 let output_format
= extract_output_format(&mut param
);
915 let (config
, _digest
) = config
::drive
::config()?
;
917 param
["drive"] = extract_drive_name(&mut param
, &config
)?
.into();
919 let mut client
= connect_to_localhost()?
;
921 let result
= client
.post("api2/json/tape/restore", Some(param
)).await?
;
923 view_task_result(&mut client
, result
, &output_format
).await?
;
932 schema
: DRIVE_NAME_SCHEMA
,
936 description
: "Force overriding existing index.",
941 description
: "Re-read the whole tape to reconstruct the catalog instead of restoring saved versions.",
946 description
: "Verbose mode - log all found chunks.",
951 schema
: OUTPUT_FORMAT
,
957 /// Scan media and record content
958 async
fn catalog_media(mut param
: Value
) -> Result
<(), Error
> {
960 let output_format
= extract_output_format(&mut param
);
962 let (config
, _digest
) = config
::drive
::config()?
;
964 let drive
= extract_drive_name(&mut param
, &config
)?
;
966 let mut client
= connect_to_localhost()?
;
968 let path
= format
!("api2/json/tape/drive/{}/catalog", drive
);
969 let result
= client
.post(&path
, Some(param
)).await?
;
971 view_task_result(&mut client
, result
, &output_format
).await?
;
978 let cmd_def
= CliCommandMap
::new()
981 CliCommand
::new(&API_METHOD_BACKUP
)
982 .arg_param(&["store", "pool"])
983 .completion_cb("drive", complete_drive_name
)
984 .completion_cb("store", complete_datastore_name
)
985 .completion_cb("pool", complete_pool_name
)
989 CliCommand
::new(&API_METHOD_RESTORE
)
990 .arg_param(&["media-set", "store", "snapshots"])
991 .completion_cb("store", complete_datastore_name
)
992 .completion_cb("media-set", complete_media_set_uuid
)
993 .completion_cb("snapshots", complete_media_set_snapshots
)
997 CliCommand
::new(&API_METHOD_BARCODE_LABEL_MEDIA
)
998 .completion_cb("drive", complete_drive_name
)
999 .completion_cb("pool", complete_pool_name
)
1003 CliCommand
::new(&API_METHOD_REWIND
)
1004 .completion_cb("drive", complete_drive_name
)
1008 CliCommand
::new(&API_METHOD_DEBUG_SCAN
)
1009 .completion_cb("drive", complete_drive_name
)
1013 CliCommand
::new(&API_METHOD_STATUS
)
1014 .completion_cb("drive", complete_drive_name
)
1018 CliCommand
::new(&API_METHOD_MOVE_TO_EOM
)
1019 .completion_cb("drive", complete_drive_name
)
1023 CliCommand
::new(&API_METHOD_FORMAT_MEDIA
)
1024 .completion_cb("drive", complete_drive_name
)
1028 CliCommand
::new(&API_METHOD_EJECT_MEDIA
)
1029 .completion_cb("drive", complete_drive_name
)
1033 CliCommand
::new(&API_METHOD_INVENTORY
)
1034 .completion_cb("drive", complete_drive_name
)
1038 CliCommand
::new(&API_METHOD_READ_LABEL
)
1039 .completion_cb("drive", complete_drive_name
)
1043 CliCommand
::new(&API_METHOD_CATALOG_MEDIA
)
1044 .completion_cb("drive", complete_drive_name
)
1048 CliCommand
::new(&API_METHOD_CARTRIDGE_MEMORY
)
1049 .completion_cb("drive", complete_drive_name
)
1052 "volume-statistics",
1053 CliCommand
::new(&API_METHOD_VOLUME_STATISTICS
)
1054 .completion_cb("drive", complete_drive_name
)
1058 CliCommand
::new(&API_METHOD_CLEAN_DRIVE
)
1059 .completion_cb("drive", complete_drive_name
)
1063 CliCommand
::new(&API_METHOD_LABEL_MEDIA
)
1064 .completion_cb("drive", complete_drive_name
)
1065 .completion_cb("pool", complete_pool_name
)
1068 .insert("changer", changer_commands())
1069 .insert("drive", drive_commands())
1070 .insert("pool", pool_commands())
1071 .insert("media", media_commands())
1072 .insert("key", encryption_key_commands())
1073 .insert("backup-job", backup_job_commands())
1076 CliCommand
::new(&API_METHOD_LOAD_MEDIA
)
1077 .arg_param(&["label-text"])
1078 .completion_cb("drive", complete_drive_name
)
1079 .completion_cb("label-text", complete_media_label_text
)
1082 "load-media-from-slot",
1083 CliCommand
::new(&API_METHOD_LOAD_MEDIA_FROM_SLOT
)
1084 .arg_param(&["source-slot"])
1085 .completion_cb("drive", complete_drive_name
)
1089 CliCommand
::new(&API_METHOD_UNLOAD_MEDIA
)
1090 .completion_cb("drive", complete_drive_name
)
1094 CliCommand
::new(&API_METHOD_EXPORT_MEDIA
)
1095 .arg_param(&["label-text"])
1096 .completion_cb("drive", complete_drive_name
)
1097 .completion_cb("label-text", complete_media_label_text
)
1101 let mut rpcenv
= CliEnvironment
::new();
1102 rpcenv
.set_auth_id(Some(String
::from("root@pam")));
1104 pbs_runtime
::main(run_async_cli_command(cmd_def
, rpcenv
));