]> git.proxmox.com Git - proxmox-backup.git/blobdiff - src/bin/proxmox-tape.rs
drop pbs_tools::auth
[proxmox-backup.git] / src / bin / proxmox-tape.rs
index 4f680e5c0fb20ccb1ff85b442b97f5f85e2e6835..98d28c9569357d45c35cc7403e984952687b55d1 100644 (file)
@@ -5,73 +5,58 @@ use proxmox::{
     api::{
         api,
         cli::*,
-        ApiHandler,
         RpcEnvironment,
         section_config::SectionConfigData,
     },
     tools::{
-        Uuid,
         time::strftime_local,
         io::ReadExt,
     },
 };
 
+use pbs_client::view_task_result;
+use pbs_tools::format::{
+    HumanByte,
+    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,
+};
+use pbs_tape::{
+    PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0, BlockReadError, MediaContentHeader,
+};
+
 use proxmox_backup::{
-    tools::format::{
-        HumanByte,
-        render_epoch,
-    },
-    server::{
-        UPID,
-        worker_is_active_local,
-    },
-    api2::{
-        self,
-        types::{
-            DRIVE_NAME_SCHEMA,
-            MEDIA_LABEL_SCHEMA,
-            MEDIA_POOL_NAME_SCHEMA,
-        },
-    },
-    config::{
-        self,
-        drive::complete_drive_name,
-        media_pool::complete_pool_name,
-    },
+    api2,
     tape::{
-        open_drive,
-        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_BACKUP_CONTENT_HEADER_MAGIC_1_0,
-            PROXMOX_BACKUP_CONTENT_NAME,
-            MediaContentHeader,
+            proxmox_tape_magic_to_text,
         },
     },
+    client_helpers::connect_to_localhost,
 };
 
 mod proxmox_tape;
 use proxmox_tape::*;
 
-// Note: local workers should print logs to stdout, so there is no need
-// to fetch/display logs. We just wait for the worker to finish.
-pub async fn wait_for_local_worker(upid_str: &str) -> Result<(), Error> {
-
-    let upid: UPID = upid_str.parse()?;
-
-    let sleep_duration = core::time::Duration::new(0, 100_000_000);
-
-    loop {
-        if worker_is_active_local(&upid) {
-            tokio::time::delay_for(sleep_duration).await;
-        } else {
-            break;
-        }
-    }
-    Ok(())
-}
-
-fn lookup_drive_name(
-    param: &Value,
+pub fn extract_drive_name(
+    param: &mut Value,
     config: &SectionConfigData,
 ) -> Result<String, Error> {
 
@@ -97,6 +82,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)
 }
 
@@ -113,27 +102,28 @@ fn lookup_drive_name(
                 optional: true,
                 default: true,
             },
-        },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
+       },
     },
 )]
-/// Erase media
-async 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)?;
 
-    let result = match info.handler {
-        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
-        _ => unreachable!(),
-    };
+    let mut client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/format-media", drive);
+    let result = client.post(&path, Some(param)).await?;
 
-    wait_for_local_worker(result.as_str().unwrap()).await?;
+    view_task_result(&mut client, result, &output_format).await?;
 
     Ok(())
 }
@@ -145,27 +135,28 @@ async fn erase_media(
                 schema: DRIVE_NAME_SCHEMA,
                 optional: true,
             },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
         },
     },
 )]
 /// Rewind tape
-async 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)?;
 
-    let result = match info.handler {
-        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
-        _ => unreachable!(),
-    };
+    let mut client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/rewind", drive);
+    let result = client.post(&path, Some(param)).await?;
 
-    wait_for_local_worker(result.as_str().unwrap()).await?;
+    view_task_result(&mut client, result, &output_format).await?;
 
     Ok(())
 }
@@ -177,25 +168,64 @@ async fn rewind(
                 schema: DRIVE_NAME_SCHEMA,
                 optional: true,
             },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
         },
     },
 )]
 /// Eject/Unload drive media
-async 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::Async(handler) => (handler)(param, info, rpcenv).await?,
-        _ => unreachable!(),
-    };
+    let mut 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(&mut client, result, &output_format).await?;
+
+    Ok(())
+}
+
+#[api(
+    input: {
+        properties: {
+            drive: {
+                schema: DRIVE_NAME_SCHEMA,
+                optional: true,
+            },
+            "label-text": {
+                schema: MEDIA_LABEL_SCHEMA,
+            },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
+        },
+    },
+)]
+/// Load media with specified label
+async fn load_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 mut 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(&mut client, result, &output_format).await?;
 
     Ok(())
 }
@@ -207,28 +237,92 @@ async fn eject_media(
                 schema: DRIVE_NAME_SCHEMA,
                 optional: true,
             },
-            "changer-id": {
+            "label-text": {
                 schema: MEDIA_LABEL_SCHEMA,
             },
         },
     },
 )]
-/// Load media
-async fn load_media(
-    mut param: Value,
-    rpcenv: &mut dyn RpcEnvironment,
-) -> Result<(), Error> {
+/// Export media with specified label
+async fn export_media(mut param: Value) -> Result<(), Error> {
 
-    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 info = &api2::tape::drive::API_METHOD_LOAD_MEDIA;
+    let mut client = connect_to_localhost()?;
 
-    match info.handler {
-        ApiHandler::Async(handler) => (handler)(param, info, rpcenv).await?,
-        _ => unreachable!(),
-    };
+    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 mut 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 mut client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/unload", drive);
+    let result = client.post(&path, Some(param)).await?;
+
+    view_task_result(&mut client, result, &output_format).await?;
 
     Ok(())
 }
@@ -244,30 +338,31 @@ async fn load_media(
                 schema: DRIVE_NAME_SCHEMA,
                 optional: true,
             },
-            "changer-id": {
+            "label-text": {
                 schema: MEDIA_LABEL_SCHEMA,
             },
-       },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
+        },
     },
 )]
 /// Label media
-async 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)?;
 
-    let result = match info.handler {
-        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
-        _ => unreachable!(),
-    };
+    let mut client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/label-media", drive);
+    let result = client.post(&path, Some(param)).await?;
 
-    wait_for_local_worker(result.as_str().unwrap()).await?;
+    view_task_result(&mut client, result, &output_format).await?;
 
     Ok(())
 }
@@ -279,7 +374,12 @@ async fn label_media(
                 schema: DRIVE_NAME_SCHEMA,
                 optional: true,
             },
-             "output-format": {
+            inventorize: {
+                description: "Inventorize media",
+                type: bool,
+                optional: true,
+            },
+            "output-format": {
                 schema: OUTPUT_FORMAT,
                 optional: true,
              },
@@ -287,32 +387,33 @@ async fn label_media(
     },
 )]
 /// Read media label
-async 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) = pbs_config::drive::config()?;
 
-    let (config, _digest) = config::drive::config()?;
+    let drive = extract_drive_name(&mut param, &config)?;
 
-    param["drive"] = lookup_drive_name(&param, &config)?.into();
+    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::Async(handler) => (handler)(param, info, rpcenv).await?,
-        _ => 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(())
 }
@@ -345,46 +446,42 @@ async fn read_label(
 async fn inventory(
     read_labels: Option<bool>,
     read_all_labels: Option<bool>,
-    param: Value,
-    rpcenv: &mut dyn RpcEnvironment,
+    mut param: Value,
 ) -> Result<(), Error> {
 
-    let output_format = get_output_format(&param);
+    let output_format = extract_output_format(&mut param);
 
-    let (config, _digest) = config::drive::config()?;
-    let drive = lookup_drive_name(&param, &config)?;
+    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 mut client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/inventory", drive);
+
     if do_read {
-        let mut param = json!({
-            "drive": &drive,
-        });
+
+        let mut param = json!({});
         if let Some(true) = read_all_labels {
             param["read-all-labels"] = true.into();
         }
-        let info = &api2::tape::drive::API_METHOD_UPDATE_INVENTORY;
-        let result = match info.handler {
-            ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
-            _ => unreachable!(),
-        };
-        wait_for_local_worker(result.as_str().unwrap()).await?;
+
+        let result = client.put(&path, Some(param)).await?; // update inventory
+        view_task_result(&mut client, result, &output_format).await?;
     }
 
-    let info = &api2::tape::drive::API_METHOD_INVENTORY;
+    let mut result = client.get(&path, None).await?;
+    let mut data = result["data"].take();
 
-    let param = json!({ "drive": &drive });
-    let mut data = match info.handler {
-        ApiHandler::Async(handler) => (handler)(param, info, rpcenv).await?,
-        _ => unreachable!(),
-    };
+    let info = &api2::tape::drive::API_METHOD_INVENTORY;
 
     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(())
 }
@@ -400,27 +497,28 @@ async fn inventory(
                 schema: DRIVE_NAME_SCHEMA,
                 optional: true,
             },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
         },
     },
 )]
 /// Label media with barcodes from changer device
-async fn barcode_label_media(
-    mut param: Value,
-    rpcenv: &mut dyn RpcEnvironment,
-) -> Result<(), Error> {
+async fn barcode_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_BARCODE_LABEL_MEDIA;
+    let drive = extract_drive_name(&mut param, &config)?;
 
-    let result = match info.handler {
-        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
-        _ => unreachable!(),
-    };
+    let mut client = connect_to_localhost()?;
 
-    wait_for_local_worker(result.as_str().unwrap()).await?;
+    let path = format!("api2/json/tape/drive/{}/barcode-label-media", drive);
+    let result = client.post(&path, Some(param)).await?;
+
+    view_task_result(&mut client, result, &output_format).await?;
 
     Ok(())
 }
@@ -436,14 +534,18 @@ async fn barcode_label_media(
     },
 )]
 /// Move to end of media (MTEOM, used to debug)
-fn move_to_eom(param: Value) -> Result<(), Error> {
+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 (config, _digest) = config::drive::config()?;
+    let _lock = lock_tape_device(&config, &drive)?;
+    set_tape_device_state(&drive, "moving to eom")?;
 
-    let drive = lookup_drive_name(&param, &config)?;
     let mut drive = open_drive(&config, &drive)?;
 
-    drive.move_to_eom()?;
+    drive.move_to_eom(false)?;
 
     Ok(())
 }
@@ -462,11 +564,15 @@ fn move_to_eom(param: Value) -> Result<(), Error> {
 ///
 /// Note: This reads unless the driver returns an IO Error, so this
 /// method is expected to fails when we reach EOT.
-fn debug_scan(param: Value) -> Result<(), Error> {
+fn debug_scan(mut param: Value) -> Result<(), Error> {
 
-    let (config, _digest) = config::drive::config()?;
+    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 drive = lookup_drive_name(&param, &config)?;
     let mut drive = open_drive(&config, &drive)?;
 
     println!("rewinding tape");
@@ -475,12 +581,19 @@ fn debug_scan(param: Value) -> Result<(), Error> {
     loop {
         let file_number = drive.current_file_number()?;
 
-        match drive.read_next_file()? {
-            None => {
-                println!("EOD");
+        match drive.read_next_file() {
+            Err(BlockReadError::EndOfFile) => {
+                println!("filemark number {}", file_number);
                 continue;
-            },
-            Some(mut reader) => {
+            }
+            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() };
@@ -488,32 +601,394 @@ fn debug_scan(param: Value) -> Result<(), Error> {
                     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 {
-                            if let Some(name) = PROXMOX_BACKUP_CONTENT_NAME.get(&header.content_magic) {
-                                println!("got content header: {}", name);
-                                println!("  uuid:  {}", Uuid::from(header.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);
-                            }
+                            println!("got unknown content header: {:?}", header.content_magic);
                         }
                     }
                     Err(err) => {
                         println!("unable to read content header - {}", err);
                     }
                 }
-                let bytes = reader.skip_to_end()?;
+                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 client = connect_to_localhost()?;
+
+    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_CARTRIDGE_MEMORY;
+
+    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 mut client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/clean", drive);
+    let result = client.put(&path, Some(param)).await?;
+
+    view_task_result(&mut 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,
+            },
+            "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 mut client = connect_to_localhost()?;
+
+    let result = client.post("api2/json/tape/backup", Some(param)).await?;
+
+    view_task_result(&mut 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 mut client = connect_to_localhost()?;
+
+    let result = client.post("api2/json/tape/restore", Some(param)).await?;
+
+    view_task_result(&mut 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 mut client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/catalog", drive);
+    let result = client.post(&path, Some(param)).await?;
+
+    view_task_result(&mut 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)
+        )
+        .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)
@@ -530,14 +1005,19 @@ fn main() {
             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(
-            "erase",
-            CliCommand::new(&API_METHOD_ERASE_MEDIA)
+            "format",
+            CliCommand::new(&API_METHOD_FORMAT_MEDIA)
                 .completion_cb("drive", complete_drive_name)
         )
         .insert(
@@ -555,6 +1035,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)
@@ -566,17 +1066,37 @@ fn main() {
         .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));
+    pbs_runtime::main(run_async_cli_command(cmd_def, rpcenv));
 }