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
,
44 complete_media_label_text
,
45 complete_media_set_uuid
,
47 PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0
,
48 PROXMOX_BACKUP_CONTENT_NAME
,
57 pub fn lookup_drive_name(
59 config
: &SectionConfigData
,
60 ) -> Result
<String
, Error
> {
62 let drive
= param
["drive"]
65 .or_else(|| std
::env
::var("PROXMOX_TAPE_DRIVE").ok())
68 let mut drive_names
= Vec
::new();
70 for (name
, (section_type
, _
)) in config
.sections
.iter() {
72 if !(section_type
== "linux" || section_type
== "virtual") { continue; }
73 drive_names
.push(name
);
76 if drive_names
.len() == 1 {
77 Some(drive_names
[0].to_owned())
82 .ok_or_else(|| format_err
!("unable to get (default) drive name"))?
;
91 schema
: DRIVE_NAME_SCHEMA
,
95 description
: "Use fast erase.",
101 schema
: OUTPUT_FORMAT
,
108 async
fn erase_media(param
: Value
) -> Result
<(), Error
> {
110 let output_format
= get_output_format(¶m
);
112 let (config
, _digest
) = config
::drive
::config()?
;
114 let drive
= lookup_drive_name(¶m
, &config
)?
;
116 let mut client
= connect_to_localhost()?
;
118 let path
= format
!("api2/json/tape/drive/{}/erase-media", drive
);
119 let result
= client
.post(&path
, Some(param
)).await?
;
121 view_task_result(&mut client
, result
, &output_format
).await?
;
130 schema
: DRIVE_NAME_SCHEMA
,
134 schema
: OUTPUT_FORMAT
,
141 async
fn rewind(param
: Value
) -> Result
<(), Error
> {
143 let output_format
= get_output_format(¶m
);
145 let (config
, _digest
) = config
::drive
::config()?
;
147 let drive
= lookup_drive_name(¶m
, &config
)?
;
149 let mut client
= connect_to_localhost()?
;
151 let path
= format
!("api2/json/tape/drive/{}/rewind", drive
);
152 let result
= client
.post(&path
, Some(param
)).await?
;
154 view_task_result(&mut client
, result
, &output_format
).await?
;
163 schema
: DRIVE_NAME_SCHEMA
,
167 schema
: OUTPUT_FORMAT
,
173 /// Eject/Unload drive media
174 async
fn eject_media(param
: Value
) -> Result
<(), Error
> {
176 let output_format
= get_output_format(¶m
);
178 let (config
, _digest
) = config
::drive
::config()?
;
180 let drive
= lookup_drive_name(¶m
, &config
)?
;
182 let mut client
= connect_to_localhost()?
;
184 let path
= format
!("api2/json/tape/drive/{}/eject-media", drive
);
185 let result
= client
.post(&path
, Some(param
)).await?
;
187 view_task_result(&mut client
, result
, &output_format
).await?
;
196 schema
: DRIVE_NAME_SCHEMA
,
200 schema
: MEDIA_LABEL_SCHEMA
,
205 /// Load media with specified label
206 async
fn load_media(param
: Value
) -> Result
<(), Error
> {
208 let (config
, _digest
) = config
::drive
::config()?
;
210 let drive
= lookup_drive_name(¶m
, &config
)?
;
212 let mut client
= connect_to_localhost()?
;
214 let path
= format
!("api2/json/tape/drive/{}/load-media", drive
);
215 client
.put(&path
, Some(param
)).await?
;
224 schema
: DRIVE_NAME_SCHEMA
,
228 schema
: MEDIA_LABEL_SCHEMA
,
233 /// Export media with specified label
234 async
fn export_media(param
: Value
) -> Result
<(), Error
> {
236 let (config
, _digest
) = config
::drive
::config()?
;
238 let drive
= lookup_drive_name(¶m
, &config
)?
;
240 let mut client
= connect_to_localhost()?
;
242 let path
= format
!("api2/json/tape/drive/{}/export-media", drive
);
243 client
.put(&path
, Some(param
)).await?
;
252 schema
: DRIVE_NAME_SCHEMA
,
256 description
: "Source slot number.",
263 /// Load media from the specified slot
264 async
fn load_media_from_slot(param
: Value
) -> Result
<(), Error
> {
266 let (config
, _digest
) = config
::drive
::config()?
;
268 let drive
= lookup_drive_name(¶m
, &config
)?
;
270 let mut client
= connect_to_localhost()?
;
272 let path
= format
!("api2/json/tape/drive/{}/load-slot", drive
);
273 client
.put(&path
, Some(param
)).await?
;
282 schema
: DRIVE_NAME_SCHEMA
,
286 description
: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.",
294 /// Unload media via changer
295 async
fn unload_media(param
: Value
) -> Result
<(), Error
> {
297 let (config
, _digest
) = config
::drive
::config()?
;
299 let drive
= lookup_drive_name(¶m
, &config
)?
;
301 let mut client
= connect_to_localhost()?
;
303 let path
= format
!("api2/json/tape/drive/{}/unload", drive
);
304 client
.put(&path
, Some(param
)).await?
;
313 schema
: MEDIA_POOL_NAME_SCHEMA
,
317 schema
: DRIVE_NAME_SCHEMA
,
321 schema
: MEDIA_LABEL_SCHEMA
,
324 schema
: OUTPUT_FORMAT
,
331 async
fn label_media(param
: Value
) -> Result
<(), Error
> {
333 let output_format
= get_output_format(¶m
);
335 let (config
, _digest
) = config
::drive
::config()?
;
337 let drive
= lookup_drive_name(¶m
, &config
)?
;
339 let mut client
= connect_to_localhost()?
;
341 let path
= format
!("api2/json/tape/drive/{}/label-media", drive
);
342 let result
= client
.post(&path
, Some(param
)).await?
;
344 view_task_result(&mut client
, result
, &output_format
).await?
;
353 schema
: DRIVE_NAME_SCHEMA
,
357 description
: "Inventorize media",
362 schema
: OUTPUT_FORMAT
,
369 async
fn read_label(param
: Value
) -> Result
<(), Error
> {
371 let output_format
= get_output_format(¶m
);
373 let (config
, _digest
) = config
::drive
::config()?
;
375 let drive
= lookup_drive_name(¶m
, &config
)?
;
377 let client
= connect_to_localhost()?
;
379 let path
= format
!("api2/json/tape/drive/{}/read-label", drive
);
380 let mut result
= client
.get(&path
, Some(param
)).await?
;
381 let mut data
= result
["data"].take();
383 let info
= &api2
::tape
::drive
::API_METHOD_READ_LABEL
;
385 let options
= default_table_format_options()
386 .column(ColumnConfig
::new("label-text"))
387 .column(ColumnConfig
::new("uuid"))
388 .column(ColumnConfig
::new("ctime").renderer(render_epoch
))
389 .column(ColumnConfig
::new("pool"))
390 .column(ColumnConfig
::new("media-set-uuid"))
391 .column(ColumnConfig
::new("media-set-ctime").renderer(render_epoch
))
392 .column(ColumnConfig
::new("encryption-key-fingerprint"))
395 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
404 schema
: OUTPUT_FORMAT
,
408 schema
: DRIVE_NAME_SCHEMA
,
412 description
: "Load unknown tapes and try read labels",
417 description
: "Load all tapes and try read labels (even if already inventoried)",
424 /// List (and update) media labels (Changer Inventory)
426 read_labels
: Option
<bool
>,
427 read_all_labels
: Option
<bool
>,
429 ) -> Result
<(), Error
> {
431 let output_format
= get_output_format(¶m
);
433 let (config
, _digest
) = config
::drive
::config()?
;
434 let drive
= lookup_drive_name(¶m
, &config
)?
;
436 let do_read
= read_labels
.unwrap_or(false) || read_all_labels
.unwrap_or(false);
438 let mut client
= connect_to_localhost()?
;
440 let path
= format
!("api2/json/tape/drive/{}/inventory", drive
);
444 let mut param
= json
!({}
);
445 if let Some(true) = read_all_labels
{
446 param
["read-all-labels"] = true.into();
449 let result
= client
.put(&path
, Some(param
)).await?
; // update inventory
450 view_task_result(&mut client
, result
, &output_format
).await?
;
453 let mut result
= client
.get(&path
, None
).await?
;
454 let mut data
= result
["data"].take();
456 let info
= &api2
::tape
::drive
::API_METHOD_INVENTORY
;
458 let options
= default_table_format_options()
459 .column(ColumnConfig
::new("label-text"))
460 .column(ColumnConfig
::new("uuid"))
463 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
472 schema
: MEDIA_POOL_NAME_SCHEMA
,
476 schema
: DRIVE_NAME_SCHEMA
,
480 schema
: OUTPUT_FORMAT
,
486 /// Label media with barcodes from changer device
487 async
fn barcode_label_media(param
: Value
) -> Result
<(), Error
> {
489 let output_format
= get_output_format(¶m
);
491 let (config
, _digest
) = config
::drive
::config()?
;
493 let drive
= lookup_drive_name(¶m
, &config
)?
;
495 let mut client
= connect_to_localhost()?
;
497 let path
= format
!("api2/json/tape/drive/{}/barcode-label-media", drive
);
498 let result
= client
.post(&path
, Some(param
)).await?
;
500 view_task_result(&mut client
, result
, &output_format
).await?
;
509 schema
: DRIVE_NAME_SCHEMA
,
515 /// Move to end of media (MTEOM, used to debug)
516 fn move_to_eom(param
: Value
) -> Result
<(), Error
> {
518 let (config
, _digest
) = config
::drive
::config()?
;
520 let drive
= lookup_drive_name(¶m
, &config
)?
;
521 let mut drive
= open_drive(&config
, &drive
)?
;
523 drive
.move_to_eom()?
;
532 schema
: DRIVE_NAME_SCHEMA
,
538 /// Rewind, then read media contents and print debug info
540 /// Note: This reads unless the driver returns an IO Error, so this
541 /// method is expected to fails when we reach EOT.
542 fn debug_scan(param
: Value
) -> Result
<(), Error
> {
544 let (config
, _digest
) = config
::drive
::config()?
;
546 let drive
= lookup_drive_name(¶m
, &config
)?
;
547 let mut drive
= open_drive(&config
, &drive
)?
;
549 println
!("rewinding tape");
553 let file_number
= drive
.current_file_number()?
;
555 match drive
.read_next_file()?
{
560 Some(mut reader
) => {
561 println
!("got file number {}", file_number
);
563 let header
: Result
<MediaContentHeader
, _
> = unsafe { reader.read_le_value() }
;
566 if header
.magic
!= PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0
{
567 println
!("got MediaContentHeader with wrong magic: {:?}", header
.magic
);
568 } else if let Some(name
) = PROXMOX_BACKUP_CONTENT_NAME
.get(&header
.content_magic
) {
569 println
!("got content header: {}", name
);
570 println
!(" uuid: {}", header
.content_uuid());
571 println
!(" ctime: {}", strftime_local("%c", header
.ctime
)?
);
572 println
!(" hsize: {}", HumanByte
::from(header
.size
as usize));
573 println
!(" part: {}", header
.part_number
);
575 println
!("got unknown content header: {:?}", header
.content_magic
);
579 println
!("unable to read content header - {}", err
);
582 let bytes
= reader
.skip_to_end()?
;
583 println
!("skipped {}", HumanByte
::from(bytes
));
593 schema
: DRIVE_NAME_SCHEMA
,
597 schema
: OUTPUT_FORMAT
,
603 /// Read Cartridge Memory (Medium auxiliary memory attributes)
604 async
fn cartridge_memory(param
: Value
) -> Result
<(), Error
> {
606 let output_format
= get_output_format(¶m
);
608 let (config
, _digest
) = config
::drive
::config()?
;
610 let drive
= lookup_drive_name(¶m
, &config
)?
;
612 let client
= connect_to_localhost()?
;
614 let path
= format
!("api2/json/tape/drive/{}/cartridge-memory", drive
);
615 let mut result
= client
.get(&path
, Some(param
)).await?
;
616 let mut data
= result
["data"].take();
618 let info
= &api2
::tape
::drive
::API_METHOD_CARTRIDGE_MEMORY
;
620 let options
= default_table_format_options()
621 .column(ColumnConfig
::new("id"))
622 .column(ColumnConfig
::new("name"))
623 .column(ColumnConfig
::new("value"))
626 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
634 schema
: DRIVE_NAME_SCHEMA
,
638 schema
: OUTPUT_FORMAT
,
644 /// Read Volume Statistics (SCSI log page 17h)
645 async
fn volume_statistics(param
: Value
) -> Result
<(), Error
> {
647 let output_format
= get_output_format(¶m
);
649 let (config
, _digest
) = config
::drive
::config()?
;
651 let drive
= lookup_drive_name(¶m
, &config
)?
;
653 let client
= connect_to_localhost()?
;
655 let path
= format
!("api2/json/tape/drive/{}/volume-statistics", drive
);
656 let mut result
= client
.get(&path
, Some(param
)).await?
;
657 let mut data
= result
["data"].take();
659 let info
= &api2
::tape
::drive
::API_METHOD_VOLUME_STATISTICS
;
661 let options
= default_table_format_options();
663 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
672 schema
: DRIVE_NAME_SCHEMA
,
676 schema
: OUTPUT_FORMAT
,
682 /// Get drive/media status
683 async
fn status(param
: Value
) -> Result
<(), Error
> {
685 let output_format
= get_output_format(¶m
);
687 let (config
, _digest
) = config
::drive
::config()?
;
689 let drive
= lookup_drive_name(¶m
, &config
)?
;
691 let client
= connect_to_localhost()?
;
693 let path
= format
!("api2/json/tape/drive/{}/status", drive
);
694 let mut result
= client
.get(&path
, Some(param
)).await?
;
695 let mut data
= result
["data"].take();
697 let info
= &api2
::tape
::drive
::API_METHOD_STATUS
;
699 let render_percentage
= |value
: &Value
, _record
: &Value
| {
700 match value
.as_f64() {
701 Some(wearout
) => Ok(format
!("{:.2}%", wearout
*100.0)),
702 None
=> Ok(String
::from("ERROR")), // should never happen
706 let options
= default_table_format_options()
707 .column(ColumnConfig
::new("blocksize"))
708 .column(ColumnConfig
::new("density"))
709 .column(ColumnConfig
::new("status"))
710 .column(ColumnConfig
::new("options"))
711 .column(ColumnConfig
::new("alert-flags"))
712 .column(ColumnConfig
::new("file-number"))
713 .column(ColumnConfig
::new("block-number"))
714 .column(ColumnConfig
::new("manufactured").renderer(render_epoch
))
715 .column(ColumnConfig
::new("bytes-written").renderer(render_bytes_human_readable
))
716 .column(ColumnConfig
::new("bytes-read").renderer(render_bytes_human_readable
))
717 .column(ColumnConfig
::new("medium-passes"))
718 .column(ColumnConfig
::new("medium-wearout").renderer(render_percentage
))
719 .column(ColumnConfig
::new("volume-mounts"))
722 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
731 schema
: DRIVE_NAME_SCHEMA
,
735 schema
: OUTPUT_FORMAT
,
742 async
fn clean_drive(param
: Value
) -> Result
<(), Error
> {
744 let output_format
= get_output_format(¶m
);
746 let (config
, _digest
) = config
::drive
::config()?
;
748 let drive
= lookup_drive_name(¶m
, &config
)?
;
750 let mut client
= connect_to_localhost()?
;
752 let path
= format
!("api2/json/tape/drive/{}/clean-drive", drive
);
753 let result
= client
.post(&path
, Some(param
)).await?
;
755 view_task_result(&mut client
, result
, &output_format
).await?
;
764 schema
: DATASTORE_SCHEMA
,
767 schema
: MEDIA_POOL_NAME_SCHEMA
,
770 schema
: DRIVE_NAME_SCHEMA
,
774 description
: "Eject media upon job completion.",
778 "export-media-set": {
779 description
: "Export media set upon job completion.",
784 schema
: OUTPUT_FORMAT
,
790 /// Backup datastore to tape media pool
791 async
fn backup(mut param
: Value
) -> Result
<(), Error
> {
793 let output_format
= get_output_format(¶m
);
795 let (config
, _digest
) = config
::drive
::config()?
;
797 param
["drive"] = lookup_drive_name(¶m
, &config
)?
.into();
799 let mut client
= connect_to_localhost()?
;
801 let result
= client
.post("api2/json/tape/backup", Some(param
)).await?
;
803 view_task_result(&mut client
, result
, &output_format
).await?
;
812 schema
: DATASTORE_SCHEMA
,
815 schema
: DRIVE_NAME_SCHEMA
,
819 description
: "Media set UUID.",
823 schema
: OUTPUT_FORMAT
,
829 /// Restore data from media-set
830 async
fn restore(mut param
: Value
) -> Result
<(), Error
> {
832 let output_format
= get_output_format(¶m
);
834 let (config
, _digest
) = config
::drive
::config()?
;
836 param
["drive"] = lookup_drive_name(¶m
, &config
)?
.into();
838 let mut client
= connect_to_localhost()?
;
840 let result
= client
.post("api2/json/tape/restore", Some(param
)).await?
;
842 view_task_result(&mut client
, result
, &output_format
).await?
;
851 schema
: DRIVE_NAME_SCHEMA
,
855 description
: "Force overriding existing index.",
860 description
: "Verbose mode - log all found chunks.",
865 schema
: OUTPUT_FORMAT
,
871 /// Scan media and record content
872 async
fn catalog_media(param
: Value
) -> Result
<(), Error
> {
874 let output_format
= get_output_format(¶m
);
876 let (config
, _digest
) = config
::drive
::config()?
;
878 let drive
= lookup_drive_name(¶m
, &config
)?
;
880 let mut client
= connect_to_localhost()?
;
882 let path
= format
!("api2/json/tape/drive/{}/catalog", drive
);
883 let result
= client
.post(&path
, Some(param
)).await?
;
885 view_task_result(&mut client
, result
, &output_format
).await?
;
892 let cmd_def
= CliCommandMap
::new()
895 CliCommand
::new(&API_METHOD_BACKUP
)
896 .arg_param(&["store", "pool"])
897 .completion_cb("store", complete_datastore_name
)
898 .completion_cb("pool", complete_pool_name
)
902 CliCommand
::new(&API_METHOD_RESTORE
)
903 .arg_param(&["media-set", "store"])
904 .completion_cb("store", complete_datastore_name
)
905 .completion_cb("media-set", complete_media_set_uuid
)
909 CliCommand
::new(&API_METHOD_BARCODE_LABEL_MEDIA
)
910 .completion_cb("drive", complete_drive_name
)
911 .completion_cb("pool", complete_pool_name
)
915 CliCommand
::new(&API_METHOD_REWIND
)
916 .completion_cb("drive", complete_drive_name
)
920 CliCommand
::new(&API_METHOD_DEBUG_SCAN
)
921 .completion_cb("drive", complete_drive_name
)
925 CliCommand
::new(&API_METHOD_STATUS
)
926 .completion_cb("drive", complete_drive_name
)
930 CliCommand
::new(&API_METHOD_MOVE_TO_EOM
)
931 .completion_cb("drive", complete_drive_name
)
935 CliCommand
::new(&API_METHOD_ERASE_MEDIA
)
936 .completion_cb("drive", complete_drive_name
)
940 CliCommand
::new(&API_METHOD_EJECT_MEDIA
)
941 .completion_cb("drive", complete_drive_name
)
945 CliCommand
::new(&API_METHOD_INVENTORY
)
946 .completion_cb("drive", complete_drive_name
)
950 CliCommand
::new(&API_METHOD_READ_LABEL
)
951 .completion_cb("drive", complete_drive_name
)
955 CliCommand
::new(&API_METHOD_CATALOG_MEDIA
)
956 .completion_cb("drive", complete_drive_name
)
960 CliCommand
::new(&API_METHOD_CARTRIDGE_MEMORY
)
961 .completion_cb("drive", complete_drive_name
)
965 CliCommand
::new(&API_METHOD_VOLUME_STATISTICS
)
966 .completion_cb("drive", complete_drive_name
)
970 CliCommand
::new(&API_METHOD_CLEAN_DRIVE
)
971 .completion_cb("drive", complete_drive_name
)
975 CliCommand
::new(&API_METHOD_LABEL_MEDIA
)
976 .completion_cb("drive", complete_drive_name
)
977 .completion_cb("pool", complete_pool_name
)
980 .insert("changer", changer_commands())
981 .insert("drive", drive_commands())
982 .insert("pool", pool_commands())
983 .insert("media", media_commands())
984 .insert("key", encryption_key_commands())
987 CliCommand
::new(&API_METHOD_LOAD_MEDIA
)
988 .arg_param(&["label-text"])
989 .completion_cb("drive", complete_drive_name
)
990 .completion_cb("label-text", complete_media_label_text
)
993 "load-media-from-slot",
994 CliCommand
::new(&API_METHOD_LOAD_MEDIA_FROM_SLOT
)
995 .arg_param(&["source-slot"])
996 .completion_cb("drive", complete_drive_name
)
1000 CliCommand
::new(&API_METHOD_UNLOAD_MEDIA
)
1001 .completion_cb("drive", complete_drive_name
)
1005 CliCommand
::new(&API_METHOD_EXPORT_MEDIA
)
1006 .arg_param(&["label-text"])
1007 .completion_cb("drive", complete_drive_name
)
1008 .completion_cb("label-text", complete_media_label_text
)
1012 let mut rpcenv
= CliEnvironment
::new();
1013 rpcenv
.set_auth_id(Some(String
::from("root@pam")));
1015 proxmox_backup
::tools
::runtime
::main(run_async_cli_command(cmd_def
, rpcenv
));