1 use anyhow
::{format_err, Error}
;
2 use serde_json
::{json, Value}
;
10 section_config
::SectionConfigData
,
22 render_bytes_human_readable
,
27 wait_for_local_worker
,
35 MEDIA_POOL_NAME_SCHEMA
,
40 datastore
::complete_datastore_name
,
41 drive
::complete_drive_name
,
42 media_pool
::complete_pool_name
,
46 complete_media_label_text
,
47 complete_media_set_uuid
,
49 PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0
,
50 PROXMOX_BACKUP_CONTENT_NAME
,
59 pub fn lookup_drive_name(
61 config
: &SectionConfigData
,
62 ) -> Result
<String
, Error
> {
64 let drive
= param
["drive"]
67 .or_else(|| std
::env
::var("PROXMOX_TAPE_DRIVE").ok())
70 let mut drive_names
= Vec
::new();
72 for (name
, (section_type
, _
)) in config
.sections
.iter() {
74 if !(section_type
== "linux" || section_type
== "virtual") { continue; }
75 drive_names
.push(name
);
78 if drive_names
.len() == 1 {
79 Some(drive_names
[0].to_owned())
84 .ok_or_else(|| format_err
!("unable to get (default) drive name"))?
;
93 schema
: DRIVE_NAME_SCHEMA
,
97 description
: "Use fast erase.",
103 schema
: OUTPUT_FORMAT
,
110 async
fn erase_media(param
: Value
) -> Result
<(), Error
> {
112 let output_format
= get_output_format(¶m
);
114 let (config
, _digest
) = config
::drive
::config()?
;
116 let drive
= lookup_drive_name(¶m
, &config
)?
;
118 let mut client
= connect_to_localhost()?
;
120 let path
= format
!("api2/json/tape/drive/{}/erase-media", drive
);
121 let result
= client
.post(&path
, Some(param
)).await?
;
123 view_task_result(client
, result
, &output_format
).await?
;
132 schema
: DRIVE_NAME_SCHEMA
,
136 schema
: OUTPUT_FORMAT
,
143 async
fn rewind(param
: Value
) -> Result
<(), Error
> {
145 let output_format
= get_output_format(¶m
);
147 let (config
, _digest
) = config
::drive
::config()?
;
149 let drive
= lookup_drive_name(¶m
, &config
)?
;
151 let mut client
= connect_to_localhost()?
;
153 let path
= format
!("api2/json/tape/drive/{}/rewind", drive
);
154 let result
= client
.post(&path
, Some(param
)).await?
;
156 view_task_result(client
, result
, &output_format
).await?
;
165 schema
: DRIVE_NAME_SCHEMA
,
169 schema
: OUTPUT_FORMAT
,
175 /// Eject/Unload drive media
176 async
fn eject_media(param
: Value
) -> Result
<(), Error
> {
178 let output_format
= get_output_format(¶m
);
180 let (config
, _digest
) = config
::drive
::config()?
;
182 let drive
= lookup_drive_name(¶m
, &config
)?
;
184 let mut client
= connect_to_localhost()?
;
186 let path
= format
!("api2/json/tape/drive/{}/eject-media", drive
);
187 let result
= client
.post(&path
, Some(param
)).await?
;
189 view_task_result(client
, result
, &output_format
).await?
;
198 schema
: DRIVE_NAME_SCHEMA
,
202 schema
: MEDIA_LABEL_SCHEMA
,
207 /// Load media with specified label
208 async
fn load_media(param
: Value
) -> Result
<(), Error
> {
210 let (config
, _digest
) = config
::drive
::config()?
;
212 let drive
= lookup_drive_name(¶m
, &config
)?
;
214 let mut client
= connect_to_localhost()?
;
216 let path
= format
!("api2/json/tape/drive/{}/load-media", drive
);
217 client
.put(&path
, Some(param
)).await?
;
226 schema
: DRIVE_NAME_SCHEMA
,
230 schema
: MEDIA_LABEL_SCHEMA
,
235 /// Export media with specified label
236 async
fn export_media(param
: Value
) -> Result
<(), Error
> {
238 let (config
, _digest
) = config
::drive
::config()?
;
240 let drive
= lookup_drive_name(¶m
, &config
)?
;
242 let mut client
= connect_to_localhost()?
;
244 let path
= format
!("api2/json/tape/drive/{}/export-media", drive
);
245 client
.put(&path
, Some(param
)).await?
;
254 schema
: DRIVE_NAME_SCHEMA
,
258 description
: "Source slot number.",
265 /// Load media from the specified slot
266 async
fn load_media_from_slot(param
: Value
) -> Result
<(), Error
> {
268 let (config
, _digest
) = config
::drive
::config()?
;
270 let drive
= lookup_drive_name(¶m
, &config
)?
;
272 let mut client
= connect_to_localhost()?
;
274 let path
= format
!("api2/json/tape/drive/{}/load-slot", drive
);
275 client
.put(&path
, Some(param
)).await?
;
284 schema
: DRIVE_NAME_SCHEMA
,
288 description
: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.",
296 /// Unload media via changer
297 async
fn unload_media(param
: Value
) -> Result
<(), Error
> {
299 let (config
, _digest
) = config
::drive
::config()?
;
301 let drive
= lookup_drive_name(¶m
, &config
)?
;
303 let mut client
= connect_to_localhost()?
;
305 let path
= format
!("api2/json/tape/drive/{}/unload", drive
);
306 client
.put(&path
, Some(param
)).await?
;
315 schema
: MEDIA_POOL_NAME_SCHEMA
,
319 schema
: DRIVE_NAME_SCHEMA
,
323 schema
: MEDIA_LABEL_SCHEMA
,
326 schema
: OUTPUT_FORMAT
,
333 async
fn label_media(param
: Value
) -> Result
<(), Error
> {
335 let output_format
= get_output_format(¶m
);
337 let (config
, _digest
) = config
::drive
::config()?
;
339 let drive
= lookup_drive_name(¶m
, &config
)?
;
341 let mut client
= connect_to_localhost()?
;
343 let path
= format
!("api2/json/tape/drive/{}/label-media", drive
);
344 let result
= client
.post(&path
, Some(param
)).await?
;
346 view_task_result(client
, result
, &output_format
).await?
;
355 schema
: DRIVE_NAME_SCHEMA
,
359 description
: "Inventorize media",
364 schema
: OUTPUT_FORMAT
,
371 async
fn read_label(param
: Value
) -> Result
<(), Error
> {
373 let output_format
= get_output_format(¶m
);
375 let (config
, _digest
) = config
::drive
::config()?
;
377 let drive
= lookup_drive_name(¶m
, &config
)?
;
379 let client
= connect_to_localhost()?
;
381 let path
= format
!("api2/json/tape/drive/{}/read-label", drive
);
382 let mut result
= client
.get(&path
, Some(param
)).await?
;
383 let mut data
= result
["data"].take();
385 let info
= &api2
::tape
::drive
::API_METHOD_READ_LABEL
;
387 let options
= default_table_format_options()
388 .column(ColumnConfig
::new("label-text"))
389 .column(ColumnConfig
::new("uuid"))
390 .column(ColumnConfig
::new("ctime").renderer(render_epoch
))
391 .column(ColumnConfig
::new("pool"))
392 .column(ColumnConfig
::new("media-set-uuid"))
393 .column(ColumnConfig
::new("media-set-ctime").renderer(render_epoch
))
394 .column(ColumnConfig
::new("encryption-key-fingerprint"))
397 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
406 schema
: OUTPUT_FORMAT
,
410 schema
: DRIVE_NAME_SCHEMA
,
414 description
: "Load unknown tapes and try read labels",
419 description
: "Load all tapes and try read labels (even if already inventoried)",
426 /// List (and update) media labels (Changer Inventory)
428 read_labels
: Option
<bool
>,
429 read_all_labels
: Option
<bool
>,
431 rpcenv
: &mut dyn RpcEnvironment
,
432 ) -> Result
<(), Error
> {
434 let output_format
= get_output_format(¶m
);
436 let (config
, _digest
) = config
::drive
::config()?
;
437 let drive
= lookup_drive_name(¶m
, &config
)?
;
439 let do_read
= read_labels
.unwrap_or(false) || read_all_labels
.unwrap_or(false);
442 let mut param
= json
!({
445 if let Some(true) = read_all_labels
{
446 param
["read-all-labels"] = true.into();
448 let info
= &api2
::tape
::drive
::API_METHOD_UPDATE_INVENTORY
;
449 let result
= match info
.handler
{
450 ApiHandler
::Sync(handler
) => (handler
)(param
, info
, rpcenv
)?
,
453 wait_for_local_worker(result
.as_str().unwrap()).await?
;
456 let info
= &api2
::tape
::drive
::API_METHOD_INVENTORY
;
458 let param
= json
!({ "drive": &drive }
);
459 let mut data
= match info
.handler
{
460 ApiHandler
::Async(handler
) => (handler
)(param
, info
, rpcenv
).await?
,
464 let options
= default_table_format_options()
465 .column(ColumnConfig
::new("label-text"))
466 .column(ColumnConfig
::new("uuid"))
469 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
478 schema
: MEDIA_POOL_NAME_SCHEMA
,
482 schema
: DRIVE_NAME_SCHEMA
,
488 /// Label media with barcodes from changer device
489 async
fn barcode_label_media(
491 rpcenv
: &mut dyn RpcEnvironment
,
492 ) -> Result
<(), Error
> {
494 let (config
, _digest
) = config
::drive
::config()?
;
496 param
["drive"] = lookup_drive_name(¶m
, &config
)?
.into();
498 let info
= &api2
::tape
::drive
::API_METHOD_BARCODE_LABEL_MEDIA
;
500 let result
= match info
.handler
{
501 ApiHandler
::Sync(handler
) => (handler
)(param
, info
, rpcenv
)?
,
505 wait_for_local_worker(result
.as_str().unwrap()).await?
;
514 schema
: DRIVE_NAME_SCHEMA
,
520 /// Move to end of media (MTEOM, used to debug)
521 fn move_to_eom(param
: Value
) -> Result
<(), Error
> {
523 let (config
, _digest
) = config
::drive
::config()?
;
525 let drive
= lookup_drive_name(¶m
, &config
)?
;
526 let mut drive
= open_drive(&config
, &drive
)?
;
528 drive
.move_to_eom()?
;
537 schema
: DRIVE_NAME_SCHEMA
,
543 /// Rewind, then read media contents and print debug info
545 /// Note: This reads unless the driver returns an IO Error, so this
546 /// method is expected to fails when we reach EOT.
547 fn debug_scan(param
: Value
) -> Result
<(), Error
> {
549 let (config
, _digest
) = config
::drive
::config()?
;
551 let drive
= lookup_drive_name(¶m
, &config
)?
;
552 let mut drive
= open_drive(&config
, &drive
)?
;
554 println
!("rewinding tape");
558 let file_number
= drive
.current_file_number()?
;
560 match drive
.read_next_file()?
{
565 Some(mut reader
) => {
566 println
!("got file number {}", file_number
);
568 let header
: Result
<MediaContentHeader
, _
> = unsafe { reader.read_le_value() }
;
571 if header
.magic
!= PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0
{
572 println
!("got MediaContentHeader with wrong magic: {:?}", header
.magic
);
573 } else if let Some(name
) = PROXMOX_BACKUP_CONTENT_NAME
.get(&header
.content_magic
) {
574 println
!("got content header: {}", name
);
575 println
!(" uuid: {}", header
.content_uuid());
576 println
!(" ctime: {}", strftime_local("%c", header
.ctime
)?
);
577 println
!(" hsize: {}", HumanByte
::from(header
.size
as usize));
578 println
!(" part: {}", header
.part_number
);
580 println
!("got unknown content header: {:?}", header
.content_magic
);
584 println
!("unable to read content header - {}", err
);
587 let bytes
= reader
.skip_to_end()?
;
588 println
!("skipped {}", HumanByte
::from(bytes
));
598 schema
: DRIVE_NAME_SCHEMA
,
602 schema
: OUTPUT_FORMAT
,
608 /// Read Cartridge Memory (Medium auxiliary memory attributes)
611 rpcenv
: &mut dyn RpcEnvironment
,
612 ) -> Result
<(), Error
> {
614 let (config
, _digest
) = config
::drive
::config()?
;
616 param
["drive"] = lookup_drive_name(¶m
, &config
)?
.into();
618 let output_format
= get_output_format(¶m
);
619 let info
= &api2
::tape
::drive
::API_METHOD_CARTRIDGE_MEMORY
;
621 let mut data
= match info
.handler
{
622 ApiHandler
::Sync(handler
) => (handler
)(param
, info
, rpcenv
)?
,
626 let options
= default_table_format_options()
627 .column(ColumnConfig
::new("id"))
628 .column(ColumnConfig
::new("name"))
629 .column(ColumnConfig
::new("value"))
632 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
640 schema
: DRIVE_NAME_SCHEMA
,
644 schema
: OUTPUT_FORMAT
,
650 /// Read Volume Statistics (SCSI log page 17h)
651 fn volume_statistics(
653 rpcenv
: &mut dyn RpcEnvironment
,
654 ) -> Result
<(), Error
> {
656 let (config
, _digest
) = config
::drive
::config()?
;
658 param
["drive"] = lookup_drive_name(¶m
, &config
)?
.into();
660 let output_format
= get_output_format(¶m
);
661 let info
= &api2
::tape
::drive
::API_METHOD_VOLUME_STATISTICS
;
663 let mut data
= match info
.handler
{
664 ApiHandler
::Sync(handler
) => (handler
)(param
, info
, rpcenv
)?
,
668 let options
= default_table_format_options();
670 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
678 schema
: DRIVE_NAME_SCHEMA
,
682 schema
: OUTPUT_FORMAT
,
688 /// Get drive/media status
691 rpcenv
: &mut dyn RpcEnvironment
,
692 ) -> Result
<(), Error
> {
694 let (config
, _digest
) = config
::drive
::config()?
;
696 param
["drive"] = lookup_drive_name(¶m
, &config
)?
.into();
698 let output_format
= get_output_format(¶m
);
699 let info
= &api2
::tape
::drive
::API_METHOD_STATUS
;
701 let mut data
= match info
.handler
{
702 ApiHandler
::Sync(handler
) => (handler
)(param
, info
, rpcenv
)?
,
706 let render_percentage
= |value
: &Value
, _record
: &Value
| {
707 match value
.as_f64() {
708 Some(wearout
) => Ok(format
!("{:.2}%", wearout
*100.0)),
709 None
=> Ok(String
::from("ERROR")), // should never happen
713 let options
= default_table_format_options()
714 .column(ColumnConfig
::new("blocksize"))
715 .column(ColumnConfig
::new("density"))
716 .column(ColumnConfig
::new("status"))
717 .column(ColumnConfig
::new("alert-flags"))
718 .column(ColumnConfig
::new("file-number"))
719 .column(ColumnConfig
::new("block-number"))
720 .column(ColumnConfig
::new("manufactured").renderer(render_epoch
))
721 .column(ColumnConfig
::new("bytes-written").renderer(render_bytes_human_readable
))
722 .column(ColumnConfig
::new("bytes-read").renderer(render_bytes_human_readable
))
723 .column(ColumnConfig
::new("medium-passes"))
724 .column(ColumnConfig
::new("medium-wearout").renderer(render_percentage
))
725 .column(ColumnConfig
::new("volume-mounts"))
728 format_and_print_result_full(&mut data
, &info
.returns
, &output_format
, &options
);
736 schema
: DRIVE_NAME_SCHEMA
,
743 async
fn clean_drive(
745 rpcenv
: &mut dyn RpcEnvironment
,
746 ) -> Result
<(), Error
> {
748 let (config
, _digest
) = config
::drive
::config()?
;
750 param
["drive"] = lookup_drive_name(¶m
, &config
)?
.into();
752 let info
= &api2
::tape
::drive
::API_METHOD_CLEAN_DRIVE
;
754 let result
= match info
.handler
{
755 ApiHandler
::Sync(handler
) => (handler
)(param
, info
, rpcenv
)?
,
759 wait_for_local_worker(result
.as_str().unwrap()).await?
;
768 schema
: DATASTORE_SCHEMA
,
771 schema
: MEDIA_POOL_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(param
: Value
) -> Result
<(), Error
> {
793 let output_format
= get_output_format(¶m
);
795 let mut client
= connect_to_localhost()?
;
797 let result
= client
.post("api2/json/tape/backup", Some(param
)).await?
;
799 view_task_result(client
, result
, &output_format
).await?
;
807 schema
: DATASTORE_SCHEMA
,
810 description
: "Media set UUID.",
816 /// Restore data from media-set
819 rpcenv
: &mut dyn RpcEnvironment
,
820 ) -> Result
<(), Error
> {
822 let info
= &api2
::tape
::restore
::API_METHOD_RESTORE
;
824 let result
= match info
.handler
{
825 ApiHandler
::Sync(handler
) => (handler
)(param
, info
, rpcenv
)?
,
829 wait_for_local_worker(result
.as_str().unwrap()).await?
;
838 schema
: DRIVE_NAME_SCHEMA
,
842 description
: "Force overriding existing index.",
847 description
: "Verbose mode - log all found chunks.",
852 schema
: OUTPUT_FORMAT
,
858 /// Scan media and record content
859 async
fn catalog_media(
861 rpcenv
: &mut dyn RpcEnvironment
,
862 ) -> Result
<(), Error
> {
864 let (config
, _digest
) = config
::drive
::config()?
;
866 param
["drive"] = lookup_drive_name(¶m
, &config
)?
.into();
868 let info
= &api2
::tape
::drive
::API_METHOD_CATALOG_MEDIA
;
870 let result
= match info
.handler
{
871 ApiHandler
::Sync(handler
) => (handler
)(param
, info
, rpcenv
)?
,
875 wait_for_local_worker(result
.as_str().unwrap()).await?
;
882 let cmd_def
= CliCommandMap
::new()
885 CliCommand
::new(&API_METHOD_BACKUP
)
886 .arg_param(&["store", "pool"])
887 .completion_cb("store", complete_datastore_name
)
888 .completion_cb("pool", complete_pool_name
)
892 CliCommand
::new(&API_METHOD_RESTORE
)
893 .arg_param(&["media-set", "store"])
894 .completion_cb("store", complete_datastore_name
)
895 .completion_cb("media-set", complete_media_set_uuid
)
899 CliCommand
::new(&API_METHOD_BARCODE_LABEL_MEDIA
)
900 .completion_cb("drive", complete_drive_name
)
901 .completion_cb("pool", complete_pool_name
)
905 CliCommand
::new(&API_METHOD_REWIND
)
906 .completion_cb("drive", complete_drive_name
)
910 CliCommand
::new(&API_METHOD_DEBUG_SCAN
)
911 .completion_cb("drive", complete_drive_name
)
915 CliCommand
::new(&API_METHOD_STATUS
)
916 .completion_cb("drive", complete_drive_name
)
920 CliCommand
::new(&API_METHOD_MOVE_TO_EOM
)
921 .completion_cb("drive", complete_drive_name
)
925 CliCommand
::new(&API_METHOD_ERASE_MEDIA
)
926 .completion_cb("drive", complete_drive_name
)
930 CliCommand
::new(&API_METHOD_EJECT_MEDIA
)
931 .completion_cb("drive", complete_drive_name
)
935 CliCommand
::new(&API_METHOD_INVENTORY
)
936 .completion_cb("drive", complete_drive_name
)
940 CliCommand
::new(&API_METHOD_READ_LABEL
)
941 .completion_cb("drive", complete_drive_name
)
945 CliCommand
::new(&API_METHOD_CATALOG_MEDIA
)
946 .completion_cb("drive", complete_drive_name
)
950 CliCommand
::new(&API_METHOD_CARTRIDGE_MEMORY
)
951 .completion_cb("drive", complete_drive_name
)
955 CliCommand
::new(&API_METHOD_VOLUME_STATISTICS
)
956 .completion_cb("drive", complete_drive_name
)
960 CliCommand
::new(&API_METHOD_CLEAN_DRIVE
)
961 .completion_cb("drive", complete_drive_name
)
965 CliCommand
::new(&API_METHOD_LABEL_MEDIA
)
966 .completion_cb("drive", complete_drive_name
)
967 .completion_cb("pool", complete_pool_name
)
970 .insert("changer", changer_commands())
971 .insert("drive", drive_commands())
972 .insert("pool", pool_commands())
973 .insert("media", media_commands())
974 .insert("key", encryption_key_commands())
977 CliCommand
::new(&API_METHOD_LOAD_MEDIA
)
978 .arg_param(&["label-text"])
979 .completion_cb("drive", complete_drive_name
)
980 .completion_cb("label-text", complete_media_label_text
)
983 "load-media-from-slot",
984 CliCommand
::new(&API_METHOD_LOAD_MEDIA_FROM_SLOT
)
985 .arg_param(&["source-slot"])
986 .completion_cb("drive", complete_drive_name
)
990 CliCommand
::new(&API_METHOD_UNLOAD_MEDIA
)
991 .completion_cb("drive", complete_drive_name
)
995 CliCommand
::new(&API_METHOD_EXPORT_MEDIA
)
996 .arg_param(&["label-text"])
997 .completion_cb("drive", complete_drive_name
)
998 .completion_cb("label-text", complete_media_label_text
)
1002 let mut rpcenv
= CliEnvironment
::new();
1003 rpcenv
.set_auth_id(Some(String
::from("root@pam")));
1005 proxmox_backup
::tools
::runtime
::main(run_async_cli_command(cmd_def
, rpcenv
));