]> git.proxmox.com Git - proxmox-backup.git/blobdiff - src/bin/proxmox-tape.rs
pbs-client: avoid mut self in http_client methods.
[proxmox-backup.git] / src / bin / proxmox-tape.rs
index c97fed53399676f7a9fafc1c03cae71445cdd28b..1f90445279153a4c6a1335798b95ab357b13bb65 100644 (file)
@@ -1,41 +1,90 @@
-use anyhow::{format_err, Error};
-use serde_json::Value;
-
-use proxmox::{
-    api::{
-        api,
-        cli::*,
-        ApiHandler,
-        RpcEnvironment,
-        section_config::SectionConfigData,
-    },
+use std::collections::HashMap;
+
+use anyhow::{bail, format_err, Error};
+use serde_json::{json, Value};
+
+use proxmox_io::ReadExt;
+use proxmox_router::RpcEnvironment;
+use proxmox_router::cli::*;
+use proxmox_schema::api;
+use proxmox_section_config::SectionConfigData;
+use proxmox_time::strftime_local;
+
+use pbs_client::view_task_result;
+use pbs_tools::format::{
+    render_epoch,
+    render_bytes_human_readable,
+};
+
+use pbs_config::drive::complete_drive_name;
+use pbs_config::media_pool::complete_pool_name;
+use pbs_config::datastore::complete_datastore_name;
+
+use pbs_api_types::{
+    Userid, Authid, DATASTORE_SCHEMA, DATASTORE_MAP_LIST_SCHEMA,
+    DRIVE_NAME_SCHEMA, MEDIA_LABEL_SCHEMA, MEDIA_POOL_NAME_SCHEMA,
+    TAPE_RESTORE_SNAPSHOT_SCHEMA, GROUP_FILTER_LIST_SCHEMA, GroupListItem,
+    HumanByte
+};
+use pbs_tape::{
+    PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0, BlockReadError, MediaContentHeader,
 };
 
 use proxmox_backup::{
-    tools::format::render_epoch,
-    api2::{
-        self,
-        types::{
-            DRIVE_ID_SCHEMA,
-            MEDIA_LABEL_SCHEMA,
-            MEDIA_POOL_NAME_SCHEMA,
-        },
-    },
-    config::{
-        self,
-        drive::complete_drive_name,
-        media_pool::complete_pool_name,
-    },
+    api2,
     tape::{
-        complete_media_changer_id,
+        drive::{
+            open_drive,
+            lock_tape_device,
+            set_tape_device_state,
+        },
+        complete_media_label_text,
+        complete_media_set_uuid,
+        complete_media_set_snapshots,
+        file_formats::{
+            proxmox_tape_magic_to_text,
+        },
     },
+    client_helpers::connect_to_localhost,
 };
 
 mod proxmox_tape;
 use proxmox_tape::*;
 
-fn lookup_drive_name(
-    param: &Value,
+async fn get_backup_groups(store: &str) -> Result<Vec<GroupListItem>, Error> {
+    let client = connect_to_localhost()?;
+    let api_res = client
+        .get(&format!("api2/json/admin/datastore/{}/groups", store), None)
+        .await?;
+
+    match api_res.get("data") {
+        Some(data) => Ok(serde_json::from_value::<Vec<GroupListItem>>(data.to_owned())?),
+        None => bail!("could not get group list"),
+    }
+}
+
+// shell completion helper
+pub fn complete_datastore_group_filter(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
+
+    let mut list = Vec::new();
+
+    list.push("regex:".to_string());
+    list.push("type:ct".to_string());
+    list.push("type:host".to_string());
+    list.push("type:vm".to_string());
+
+    if let Some(store) =  param.get("store") {
+        let groups = proxmox_async::runtime::block_on(async { get_backup_groups(store).await });
+        if let Ok(groups) = groups {
+            list.extend(groups.iter().map(|group| format!("group:{}/{}", group.backup_type, group.backup_id)));
+        }
+    }
+
+    list
+}
+
+pub fn extract_drive_name(
+    param: &mut Value,
     config: &SectionConfigData,
 ) -> Result<String, Error> {
 
@@ -61,6 +110,10 @@ fn lookup_drive_name(
         })
         .ok_or_else(|| format_err!("unable to get (default) drive name"))?;
 
+    if let Some(map) = param.as_object_mut() {
+        map.remove("drive");
+    }
+
     Ok(drive)
 }
 
@@ -68,7 +121,7 @@ fn lookup_drive_name(
     input: {
         properties: {
             drive: {
-                schema: DRIVE_ID_SCHEMA,
+                schema: DRIVE_NAME_SCHEMA,
                 optional: true,
             },
             fast: {
@@ -77,25 +130,28 @@ fn lookup_drive_name(
                 optional: true,
                 default: true,
             },
-        },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
+       },
     },
 )]
-/// Erase media
-fn erase_media(
-    mut param: Value,
-    rpcenv: &mut dyn RpcEnvironment,
-) -> Result<(), Error> {
+/// Format media
+async fn format_media(mut param: Value) -> Result<(), Error> {
 
-    let (config, _digest) = config::drive::config()?;
+    let output_format = extract_output_format(&mut param);
 
-    param["drive"] = lookup_drive_name(&param, &config)?.into();
+    let (config, _digest) = pbs_config::drive::config()?;
 
-    let info = &api2::tape::drive::API_METHOD_ERASE_MEDIA;
+    let drive = extract_drive_name(&mut param, &config)?;
 
-    match info.handler {
-        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
-        _ => unreachable!(),
-    };
+    let client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/format-media", drive);
+    let result = client.post(&path, Some(param)).await?;
+
+    view_task_result(&client, result, &output_format).await?;
 
     Ok(())
 }
@@ -104,28 +160,31 @@ fn erase_media(
     input: {
         properties: {
             drive: {
-                schema: DRIVE_ID_SCHEMA,
+                schema: DRIVE_NAME_SCHEMA,
+                optional: true,
+            },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
                 optional: true,
             },
         },
     },
 )]
 /// Rewind tape
-fn rewind(
-    mut param: Value,
-    rpcenv: &mut dyn RpcEnvironment,
-) -> Result<(), Error> {
+async fn rewind(mut param: Value) -> Result<(), Error> {
 
-    let (config, _digest) = config::drive::config()?;
+    let output_format = extract_output_format(&mut param);
 
-    param["drive"] = lookup_drive_name(&param, &config)?.into();
+    let (config, _digest) = pbs_config::drive::config()?;
 
-    let info = &api2::tape::drive::API_METHOD_REWIND;
+    let drive = extract_drive_name(&mut param, &config)?;
 
-    match info.handler {
-        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
-        _ => unreachable!(),
-    };
+    let client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/rewind", drive);
+    let result = client.post(&path, Some(param)).await?;
+
+    view_task_result(&client, result, &output_format).await?;
 
     Ok(())
 }
@@ -134,28 +193,31 @@ fn rewind(
     input: {
         properties: {
             drive: {
-                schema: DRIVE_ID_SCHEMA,
+                schema: DRIVE_NAME_SCHEMA,
+                optional: true,
+            },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
                 optional: true,
             },
         },
     },
 )]
 /// Eject/Unload drive media
-fn eject_media(
-    mut param: Value,
-    rpcenv: &mut dyn RpcEnvironment,
-) -> Result<(), Error> {
+async fn eject_media(mut param: Value) -> Result<(), Error> {
 
-    let (config, _digest) = config::drive::config()?;
+    let output_format = extract_output_format(&mut param);
 
-    param["drive"] = lookup_drive_name(&param, &config)?.into();
+    let (config, _digest) = pbs_config::drive::config()?;
 
-    let info = &api2::tape::drive::API_METHOD_EJECT_MEDIA;
+    let drive = extract_drive_name(&mut param, &config)?;
 
-    match info.handler {
-        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
-        _ => unreachable!(),
-    };
+    let client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/eject-media", drive);
+    let result = client.post(&path, Some(param)).await?;
+
+    view_task_result(&client, result, &output_format).await?;
 
     Ok(())
 }
@@ -164,31 +226,131 @@ fn eject_media(
     input: {
         properties: {
             drive: {
-                schema: DRIVE_ID_SCHEMA,
+                schema: DRIVE_NAME_SCHEMA,
                 optional: true,
             },
-            "changer-id": {
+            "label-text": {
                 schema: MEDIA_LABEL_SCHEMA,
             },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
         },
     },
 )]
-/// Load media
-fn load_media(
-    mut param: Value,
-    rpcenv: &mut dyn RpcEnvironment,
-) -> Result<(), Error> {
+/// Load media with specified label
+async fn load_media(mut param: Value) -> Result<(), Error> {
 
-    let (config, _digest) = config::drive::config()?;
+    let output_format = extract_output_format(&mut param);
 
-    param["drive"] = lookup_drive_name(&param, &config)?.into();
+    let (config, _digest) = pbs_config::drive::config()?;
 
-    let info = &api2::tape::drive::API_METHOD_LOAD_MEDIA;
+    let drive = extract_drive_name(&mut param, &config)?;
 
-    match info.handler {
-        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
-        _ => unreachable!(),
-    };
+    let client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/load-media", drive);
+    let result = client.post(&path, Some(param)).await?;
+
+    view_task_result(&client, result, &output_format).await?;
+
+    Ok(())
+}
+
+#[api(
+    input: {
+        properties: {
+            drive: {
+                schema: DRIVE_NAME_SCHEMA,
+                optional: true,
+            },
+            "label-text": {
+                schema: MEDIA_LABEL_SCHEMA,
+            },
+        },
+    },
+)]
+/// Export media with specified label
+async fn export_media(mut param: Value) -> Result<(), Error> {
+
+    let (config, _digest) = pbs_config::drive::config()?;
+
+    let drive = extract_drive_name(&mut param, &config)?;
+
+    let client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/export-media", drive);
+    client.put(&path, Some(param)).await?;
+
+    Ok(())
+}
+
+#[api(
+    input: {
+        properties: {
+            drive: {
+                schema: DRIVE_NAME_SCHEMA,
+                optional: true,
+            },
+            "source-slot": {
+                description: "Source slot number.",
+                type: u64,
+                minimum: 1,
+            },
+        },
+    },
+)]
+/// Load media from the specified slot
+async fn load_media_from_slot(mut param: Value) -> Result<(), Error> {
+
+    let (config, _digest) = pbs_config::drive::config()?;
+
+    let drive = extract_drive_name(&mut param, &config)?;
+
+    let client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/load-slot", drive);
+    client.put(&path, Some(param)).await?;
+
+    Ok(())
+}
+
+#[api(
+    input: {
+        properties: {
+            drive: {
+                schema: DRIVE_NAME_SCHEMA,
+                optional: true,
+            },
+            "target-slot": {
+                description: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.",
+                type: u64,
+                minimum: 1,
+                optional: true,
+            },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
+        },
+    },
+)]
+/// Unload media via changer
+async fn unload_media(mut param: Value) -> Result<(), Error> {
+
+    let output_format = extract_output_format(&mut param);
+
+    let (config, _digest) = pbs_config::drive::config()?;
+
+    let drive = extract_drive_name(&mut param, &config)?;
+
+    let client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/unload", drive);
+    let result = client.post(&path, Some(param)).await?;
+
+    view_task_result(&client, result, &output_format).await?;
 
     Ok(())
 }
@@ -201,31 +363,34 @@ fn load_media(
                 optional: true,
             },
             drive: {
-                schema: DRIVE_ID_SCHEMA,
+                schema: DRIVE_NAME_SCHEMA,
                 optional: true,
             },
-            "changer-id": {
+            "label-text": {
                 schema: MEDIA_LABEL_SCHEMA,
             },
-       },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
+        },
     },
 )]
 /// Label media
-fn label_media(
-    mut param: Value,
-    rpcenv: &mut dyn RpcEnvironment,
-) -> Result<(), Error> {
+async fn label_media(mut param: Value) -> Result<(), Error> {
 
-    let (config, _digest) = config::drive::config()?;
+    let output_format = extract_output_format(&mut param);
 
-    param["drive"] = lookup_drive_name(&param, &config)?.into();
+    let (config, _digest) = pbs_config::drive::config()?;
 
-    let info = &api2::tape::drive::API_METHOD_LABEL_MEDIA;
+    let drive = extract_drive_name(&mut param, &config)?;
 
-    match info.handler {
-        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
-        _ => unreachable!(),
-    };
+    let client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/label-media", drive);
+    let result = client.post(&path, Some(param)).await?;
+
+    view_task_result(&client, result, &output_format).await?;
 
     Ok(())
 }
@@ -234,10 +399,15 @@ fn label_media(
     input: {
         properties: {
             drive: {
-                schema: DRIVE_ID_SCHEMA,
+                schema: DRIVE_NAME_SCHEMA,
                 optional: true,
             },
-             "output-format": {
+            inventorize: {
+                description: "Inventorize media",
+                type: bool,
+                optional: true,
+            },
+            "output-format": {
                 schema: OUTPUT_FORMAT,
                 optional: true,
              },
@@ -245,32 +415,33 @@ fn label_media(
     },
 )]
 /// Read media label
-fn read_label(
-    mut param: Value,
-    rpcenv: &mut dyn RpcEnvironment,
-) -> Result<(), Error> {
+async fn read_label(mut param: Value) -> Result<(), Error> {
+
+    let output_format = extract_output_format(&mut param);
 
-    let (config, _digest) = config::drive::config()?;
+    let (config, _digest) = pbs_config::drive::config()?;
 
-    param["drive"] = lookup_drive_name(&param, &config)?.into();
+    let drive = extract_drive_name(&mut param, &config)?;
+
+    let client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/read-label", drive);
+    let mut result = client.get(&path, Some(param)).await?;
+    let mut data = result["data"].take();
 
-    let output_format = get_output_format(&param);
     let info = &api2::tape::drive::API_METHOD_READ_LABEL;
-    let mut data = match info.handler {
-        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
-        _ => unreachable!(),
-    };
 
     let options = default_table_format_options()
-        .column(ColumnConfig::new("changer-id"))
+        .column(ColumnConfig::new("label-text"))
         .column(ColumnConfig::new("uuid"))
         .column(ColumnConfig::new("ctime").renderer(render_epoch))
         .column(ColumnConfig::new("pool"))
         .column(ColumnConfig::new("media-set-uuid"))
         .column(ColumnConfig::new("media-set-ctime").renderer(render_epoch))
+        .column(ColumnConfig::new("encryption-key-fingerprint"))
         ;
 
-    format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+    format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
 
     Ok(())
 }
@@ -283,7 +454,7 @@ fn read_label(
                 optional: true,
             },
             drive: {
-                schema: DRIVE_ID_SCHEMA,
+                schema: DRIVE_NAME_SCHEMA,
                 optional: true,
             },
             "read-labels": {
@@ -299,29 +470,46 @@ fn read_label(
         },
     },
 )]
-/// List or update media labels (Changer Inventory)
-fn inventory(
+/// List (and update) media labels (Changer Inventory)
+async fn inventory(
+    read_labels: Option<bool>,
+    read_all_labels: Option<bool>,
     mut param: Value,
-    rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<(), Error> {
 
-    let (config, _digest) = config::drive::config()?;
+    let output_format = extract_output_format(&mut param);
+
+    let (config, _digest) = pbs_config::drive::config()?;
+    let drive = extract_drive_name(&mut param, &config)?;
+
+    let do_read = read_labels.unwrap_or(false) || read_all_labels.unwrap_or(false);
+
+    let client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/inventory", drive);
 
-    param["drive"] = lookup_drive_name(&param, &config)?.into();
+    if do_read {
+
+        let mut param = json!({});
+        if let Some(true) = read_all_labels {
+            param["read-all-labels"] = true.into();
+        }
+
+        let result = client.put(&path, Some(param)).await?; // update inventory
+        view_task_result(&client, result, &output_format).await?;
+    }
+
+    let mut result = client.get(&path, None).await?;
+    let mut data = result["data"].take();
 
-    let output_format = get_output_format(&param);
     let info = &api2::tape::drive::API_METHOD_INVENTORY;
-    let mut data = match info.handler {
-        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
-        _ => unreachable!(),
-    };
 
     let options = default_table_format_options()
-        .column(ColumnConfig::new("changer-id"))
+        .column(ColumnConfig::new("label-text"))
         .column(ColumnConfig::new("uuid"))
         ;
 
-    format_and_print_result_full(&mut data, info.returns, &output_format, &options);
+    format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
 
     Ok(())
 }
@@ -334,35 +522,510 @@ fn inventory(
                 optional: true,
             },
             drive: {
-                schema: DRIVE_ID_SCHEMA,
+                schema: DRIVE_NAME_SCHEMA,
+                optional: true,
+            },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
                 optional: true,
             },
         },
     },
 )]
 /// Label media with barcodes from changer device
-fn barcode_label_media(
-    mut param: Value,
-    rpcenv: &mut dyn RpcEnvironment,
-) -> Result<(), Error> {
+async fn barcode_label_media(mut param: Value) -> Result<(), Error> {
+
+    let output_format = extract_output_format(&mut param);
+
+    let (config, _digest) = pbs_config::drive::config()?;
+
+    let drive = extract_drive_name(&mut param, &config)?;
+
+    let client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/barcode-label-media", drive);
+    let result = client.post(&path, Some(param)).await?;
+
+    view_task_result(&client, result, &output_format).await?;
+
+    Ok(())
+}
+
+#[api(
+    input: {
+        properties: {
+            drive: {
+                schema: DRIVE_NAME_SCHEMA,
+                optional: true,
+            },
+        },
+    },
+)]
+/// Move to end of media (MTEOM, used to debug)
+fn move_to_eom(mut param: Value) -> Result<(), Error> {
+
+    let (config, _digest) = pbs_config::drive::config()?;
+
+    let drive = extract_drive_name(&mut param, &config)?;
+
+    let _lock = lock_tape_device(&config, &drive)?;
+    set_tape_device_state(&drive, "moving to eom")?;
+
+    let mut drive = open_drive(&config, &drive)?;
+
+    drive.move_to_eom(false)?;
+
+    Ok(())
+}
+
+#[api(
+    input: {
+        properties: {
+            drive: {
+                schema: DRIVE_NAME_SCHEMA,
+                optional: true,
+            },
+        },
+    },
+)]
+/// Rewind, then read media contents and print debug info
+///
+/// Note: This reads unless the driver returns an IO Error, so this
+/// method is expected to fails when we reach EOT.
+fn debug_scan(mut param: Value) -> Result<(), Error> {
+
+    let (config, _digest) = pbs_config::drive::config()?;
+
+    let drive = extract_drive_name(&mut param, &config)?;
+
+    let _lock = lock_tape_device(&config, &drive)?;
+    set_tape_device_state(&drive, "debug scan")?;
+
+    let mut drive = open_drive(&config, &drive)?;
+
+    println!("rewinding tape");
+    drive.rewind()?;
+
+    loop {
+        let file_number = drive.current_file_number()?;
+
+        match drive.read_next_file() {
+            Err(BlockReadError::EndOfFile) => {
+                println!("filemark number {}", file_number);
+                continue;
+            }
+            Err(BlockReadError::EndOfStream) => {
+                println!("got EOT");
+                return Ok(());
+            }
+            Err(BlockReadError::Error(err)) => {
+                return Err(err.into());
+            }
+            Ok(mut reader) => {
+                println!("got file number {}", file_number);
+
+                let header: Result<MediaContentHeader, _> = unsafe { reader.read_le_value() };
+                match header {
+                    Ok(header) => {
+                        if header.magic != PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0 {
+                            println!("got MediaContentHeader with wrong magic: {:?}", header.magic);
+                        } else if let Some(name) = proxmox_tape_magic_to_text(&header.content_magic) {
+                            println!("got content header: {}", name);
+                            println!("  uuid:  {}", header.content_uuid());
+                            println!("  ctime: {}", strftime_local("%c", header.ctime)?);
+                            println!("  hsize: {}", HumanByte::from(header.size as usize));
+                            println!("  part:  {}", header.part_number);
+                        } else {
+                            println!("got unknown content header: {:?}", header.content_magic);
+                        }
+                    }
+                    Err(err) => {
+                        println!("unable to read content header - {}", err);
+                    }
+                }
+                let bytes = reader.skip_data()?;
+                println!("skipped {}", HumanByte::from(bytes));
+                if let Ok(true) = reader.has_end_marker() {
+                    if reader.is_incomplete()? {
+                        println!("WARNING: file is incomplete");
+                    }
+                } else {
+                    println!("WARNING: file without end marker");
+                }
+            }
+        }
+    }
+}
+
+#[api(
+    input: {
+        properties: {
+            drive: {
+                schema: DRIVE_NAME_SCHEMA,
+                optional: true,
+            },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
+        },
+    },
+)]
+/// Read Cartridge Memory (Medium auxiliary memory attributes)
+async fn cartridge_memory(mut param: Value) -> Result<(), Error> {
+
+    let output_format = extract_output_format(&mut param);
+
+    let (config, _digest) = pbs_config::drive::config()?;
+
+    let drive = extract_drive_name(&mut param, &config)?;
 
-    let (config, _digest) = config::drive::config()?;
+    let client = connect_to_localhost()?;
 
-    param["drive"] = lookup_drive_name(&param, &config)?.into();
+    let path = format!("api2/json/tape/drive/{}/cartridge-memory", drive);
+    let mut result = client.get(&path, Some(param)).await?;
+    let mut data = result["data"].take();
 
-    let info = &api2::tape::drive::API_METHOD_BARCODE_LABEL_MEDIA;
+    let info = &api2::tape::drive::API_METHOD_CARTRIDGE_MEMORY;
 
-    match info.handler {
-        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
-        _ => unreachable!(),
+    let options = default_table_format_options()
+        .column(ColumnConfig::new("id"))
+        .column(ColumnConfig::new("name"))
+        .column(ColumnConfig::new("value"))
+        ;
+
+    format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
+    Ok(())
+}
+
+#[api(
+    input: {
+        properties: {
+            drive: {
+                schema: DRIVE_NAME_SCHEMA,
+                optional: true,
+            },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
+        },
+    },
+)]
+/// Read Volume Statistics (SCSI log page 17h)
+async fn volume_statistics(mut param: Value) -> Result<(), Error> {
+
+    let output_format = extract_output_format(&mut param);
+
+    let (config, _digest) = pbs_config::drive::config()?;
+
+    let drive = extract_drive_name(&mut param, &config)?;
+
+    let client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/volume-statistics", drive);
+    let mut result = client.get(&path, Some(param)).await?;
+    let mut data = result["data"].take();
+
+    let info = &api2::tape::drive::API_METHOD_VOLUME_STATISTICS;
+
+    let options = default_table_format_options();
+
+    format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
+
+    Ok(())
+}
+
+#[api(
+    input: {
+        properties: {
+            drive: {
+                schema: DRIVE_NAME_SCHEMA,
+                optional: true,
+            },
+             "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+             },
+        },
+    },
+)]
+/// Get drive/media status
+async fn status(mut param: Value) -> Result<(), Error> {
+
+    let output_format = extract_output_format(&mut param);
+
+    let (config, _digest) = pbs_config::drive::config()?;
+
+    let drive = extract_drive_name(&mut param, &config)?;
+
+    let client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/status", drive);
+    let mut result = client.get(&path, Some(param)).await?;
+    let mut data = result["data"].take();
+
+    let info = &api2::tape::drive::API_METHOD_STATUS;
+
+    let render_percentage = |value: &Value, _record: &Value| {
+        match value.as_f64() {
+            Some(wearout) => Ok(format!("{:.2}%", wearout*100.0)),
+            None => Ok(String::from("ERROR")), // should never happen
+        }
     };
 
+    let options = default_table_format_options()
+        .column(ColumnConfig::new("blocksize"))
+        .column(ColumnConfig::new("density"))
+        .column(ColumnConfig::new("compression"))
+        .column(ColumnConfig::new("buffer-mode"))
+        .column(ColumnConfig::new("write-protect"))
+        .column(ColumnConfig::new("alert-flags"))
+        .column(ColumnConfig::new("file-number"))
+        .column(ColumnConfig::new("block-number"))
+        .column(ColumnConfig::new("manufactured").renderer(render_epoch))
+        .column(ColumnConfig::new("bytes-written").renderer(render_bytes_human_readable))
+        .column(ColumnConfig::new("bytes-read").renderer(render_bytes_human_readable))
+        .column(ColumnConfig::new("medium-passes"))
+        .column(ColumnConfig::new("medium-wearout").renderer(render_percentage))
+        .column(ColumnConfig::new("volume-mounts"))
+        ;
+
+    format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
+
+    Ok(())
+}
+
+#[api(
+    input: {
+        properties: {
+            drive: {
+                schema: DRIVE_NAME_SCHEMA,
+                optional: true,
+            },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
+        },
+    },
+)]
+/// Clean drive
+async fn clean_drive(mut param: Value) -> Result<(), Error> {
+
+    let output_format = extract_output_format(&mut param);
+
+    let (config, _digest) = pbs_config::drive::config()?;
+
+    let drive = extract_drive_name(&mut param, &config)?;
+
+    let client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/clean", drive);
+    let result = client.put(&path, Some(param)).await?;
+
+    view_task_result(&client, result, &output_format).await?;
+
+    Ok(())
+}
+
+#[api(
+    input: {
+        properties: {
+
+            // Note: We cannot use TapeBackupJobSetup, because drive needs to be optional here
+            //setup: {
+            //    type: TapeBackupJobSetup,
+            //    flatten: true,
+            //},
+
+            store: {
+                schema: DATASTORE_SCHEMA,
+            },
+            pool: {
+                schema: MEDIA_POOL_NAME_SCHEMA,
+            },
+            drive: {
+                schema: DRIVE_NAME_SCHEMA,
+                optional: true,
+            },
+            "eject-media": {
+                description: "Eject media upon job completion.",
+                type: bool,
+                optional: true,
+            },
+            "export-media-set": {
+                description: "Export media set upon job completion.",
+                type: bool,
+                optional: true,
+            },
+            "latest-only": {
+                description: "Backup latest snapshots only.",
+                type: bool,
+                optional: true,
+            },
+            "notify-user": {
+                optional: true,
+                type: Userid,
+            },
+            groups: {
+                schema: GROUP_FILTER_LIST_SCHEMA,
+                optional: true,
+            },
+            "force-media-set": {
+                description: "Ignore the allocation policy and start a new media-set.",
+                optional: true,
+                type: bool,
+                default: false,
+            },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
+        },
+    },
+)]
+/// Backup datastore to tape media pool
+async fn backup(mut param: Value) -> Result<(), Error> {
+
+    let output_format = extract_output_format(&mut param);
+
+    let (config, _digest) = pbs_config::drive::config()?;
+
+    param["drive"] = extract_drive_name(&mut param, &config)?.into();
+
+    let client = connect_to_localhost()?;
+
+    let result = client.post("api2/json/tape/backup", Some(param)).await?;
+
+    view_task_result(&client, result, &output_format).await?;
+
+    Ok(())
+}
+
+#[api(
+   input: {
+        properties: {
+            store: {
+                schema: DATASTORE_MAP_LIST_SCHEMA,
+            },
+            drive: {
+                schema: DRIVE_NAME_SCHEMA,
+                optional: true,
+            },
+            "media-set": {
+                description: "Media set UUID.",
+                type: String,
+            },
+            "notify-user": {
+                type: Userid,
+                optional: true,
+            },
+            "snapshots": {
+                description: "List of snapshots.",
+                type: Array,
+                optional: true,
+                items: {
+                    schema: TAPE_RESTORE_SNAPSHOT_SCHEMA,
+                },
+            },
+            owner: {
+                type: Authid,
+                optional: true,
+            },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
+        },
+    },
+)]
+/// Restore data from media-set
+async fn restore(mut param: Value) -> Result<(), Error> {
+
+    let output_format = extract_output_format(&mut param);
+
+    let (config, _digest) = pbs_config::drive::config()?;
+
+    param["drive"] = extract_drive_name(&mut param, &config)?.into();
+
+    let client = connect_to_localhost()?;
+
+    let result = client.post("api2/json/tape/restore", Some(param)).await?;
+
+    view_task_result(&client, result, &output_format).await?;
+
+    Ok(())
+}
+
+#[api(
+    input: {
+        properties: {
+            drive: {
+                schema: DRIVE_NAME_SCHEMA,
+                optional: true,
+            },
+            force: {
+                description: "Force overriding existing index.",
+                type: bool,
+                optional: true,
+            },
+            scan: {
+                description: "Re-read the whole tape to reconstruct the catalog instead of restoring saved versions.",
+                type: bool,
+                optional: true,
+            },
+            verbose: {
+                description: "Verbose mode - log all found chunks.",
+                type: bool,
+                optional: true,
+            },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
+        },
+    },
+)]
+/// Scan media and record content
+async fn catalog_media(mut param: Value)  -> Result<(), Error> {
+
+    let output_format = extract_output_format(&mut param);
+
+    let (config, _digest) = pbs_config::drive::config()?;
+
+    let drive = extract_drive_name(&mut param, &config)?;
+
+    let client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/catalog", drive);
+    let result = client.post(&path, Some(param)).await?;
+
+    view_task_result(&client, result, &output_format).await?;
+
     Ok(())
 }
 
 fn main() {
 
     let cmd_def = CliCommandMap::new()
+        .insert(
+            "backup",
+            CliCommand::new(&API_METHOD_BACKUP)
+                .arg_param(&["store", "pool"])
+                .completion_cb("drive", complete_drive_name)
+                .completion_cb("store", complete_datastore_name)
+                .completion_cb("pool", complete_pool_name)
+                .completion_cb("groups", complete_datastore_group_filter)
+        )
+        .insert(
+            "restore",
+            CliCommand::new(&API_METHOD_RESTORE)
+                .arg_param(&["media-set", "store", "snapshots"])
+                .completion_cb("store", complete_datastore_name)
+                .completion_cb("media-set", complete_media_set_uuid)
+                .completion_cb("snapshots", complete_media_set_snapshots)
+        )
         .insert(
             "barcode-label",
             CliCommand::new(&API_METHOD_BARCODE_LABEL_MEDIA)
@@ -375,8 +1038,23 @@ fn main() {
                 .completion_cb("drive", complete_drive_name)
         )
         .insert(
-            "erase",
-            CliCommand::new(&API_METHOD_ERASE_MEDIA)
+            "scan",
+            CliCommand::new(&API_METHOD_DEBUG_SCAN)
+                .completion_cb("drive", complete_drive_name)
+        )
+        .insert(
+            "status",
+            CliCommand::new(&API_METHOD_STATUS)
+                .completion_cb("drive", complete_drive_name)
+        )
+        .insert(
+            "eod",
+            CliCommand::new(&API_METHOD_MOVE_TO_EOM)
+                .completion_cb("drive", complete_drive_name)
+        )
+        .insert(
+            "format",
+            CliCommand::new(&API_METHOD_FORMAT_MEDIA)
                 .completion_cb("drive", complete_drive_name)
         )
         .insert(
@@ -394,6 +1072,26 @@ fn main() {
             CliCommand::new(&API_METHOD_READ_LABEL)
                 .completion_cb("drive", complete_drive_name)
         )
+        .insert(
+            "catalog",
+            CliCommand::new(&API_METHOD_CATALOG_MEDIA)
+                .completion_cb("drive", complete_drive_name)
+        )
+        .insert(
+            "cartridge-memory",
+            CliCommand::new(&API_METHOD_CARTRIDGE_MEMORY)
+                .completion_cb("drive", complete_drive_name)
+        )
+        .insert(
+            "volume-statistics",
+            CliCommand::new(&API_METHOD_VOLUME_STATISTICS)
+                .completion_cb("drive", complete_drive_name)
+        )
+        .insert(
+            "clean",
+            CliCommand::new(&API_METHOD_CLEAN_DRIVE)
+                .completion_cb("drive", complete_drive_name)
+        )
         .insert(
             "label",
             CliCommand::new(&API_METHOD_LABEL_MEDIA)
@@ -404,17 +1102,38 @@ fn main() {
         .insert("changer", changer_commands())
         .insert("drive", drive_commands())
         .insert("pool", pool_commands())
+        .insert("media", media_commands())
+        .insert("key", encryption_key_commands())
+        .insert("backup-job", backup_job_commands())
         .insert(
             "load-media",
             CliCommand::new(&API_METHOD_LOAD_MEDIA)
-                .arg_param(&["changer-id"])
+                .arg_param(&["label-text"])
+                .completion_cb("drive", complete_drive_name)
+                .completion_cb("label-text", complete_media_label_text)
+        )
+        .insert(
+            "load-media-from-slot",
+            CliCommand::new(&API_METHOD_LOAD_MEDIA_FROM_SLOT)
+                .arg_param(&["source-slot"])
+                .completion_cb("drive", complete_drive_name)
+        )
+        .insert(
+            "unload",
+            CliCommand::new(&API_METHOD_UNLOAD_MEDIA)
+                .completion_cb("drive", complete_drive_name)
+        )
+        .insert(
+            "export-media",
+            CliCommand::new(&API_METHOD_EXPORT_MEDIA)
+                .arg_param(&["label-text"])
                 .completion_cb("drive", complete_drive_name)
-                .completion_cb("changer-id", complete_media_changer_id)
+                .completion_cb("label-text", complete_media_label_text)
         )
         ;
 
     let mut rpcenv = CliEnvironment::new();
     rpcenv.set_auth_id(Some(String::from("root@pam")));
 
-    proxmox_backup::tools::runtime::main(run_async_cli_command(cmd_def, rpcenv));
+    proxmox_async::runtime::main(run_async_cli_command(cmd_def, rpcenv));
 }