]> 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 610c64d60dcc889957c3407ae4959292cb71119a..1f90445279153a4c6a1335798b95ab357b13bb65 100644 (file)
@@ -1,80 +1,90 @@
-use anyhow::{format_err, Error};
+use std::collections::HashMap;
+
+use anyhow::{bail, format_err, Error};
 use serde_json::{json, Value};
 
-use proxmox::{
-    api::{
-        api,
-        cli::*,
-        ApiHandler,
-        RpcEnvironment,
-        section_config::SectionConfigData,
-    },
-    tools::{
-        time::strftime_local,
-        io::ReadExt,
-    },
+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::{
-        HumanByte,
-        render_epoch,
-        render_bytes_human_readable,
-    },
-    server::{
-        UPID,
-        worker_is_active_local,
-    },
-    api2::{
-        self,
-        types::{
-            DATASTORE_SCHEMA,
-            DRIVE_NAME_SCHEMA,
-            MEDIA_LABEL_SCHEMA,
-            MEDIA_POOL_NAME_SCHEMA,
-        },
-    },
-    config::{
-        self,
-        datastore::complete_datastore_name,
-        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> {
+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?;
 
-    let upid: UPID = upid_str.parse()?;
+    match api_res.get("data") {
+        Some(data) => Ok(serde_json::from_value::<Vec<GroupListItem>>(data.to_owned())?),
+        None => bail!("could not get group list"),
+    }
+}
 
-    let sleep_duration = core::time::Duration::new(0, 100_000_000);
+// shell completion helper
+pub fn complete_datastore_group_filter(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
 
-    loop {
-        if worker_is_active_local(&upid) {
-            tokio::time::delay_for(sleep_duration).await;
-        } else {
-            break;
+    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)));
         }
     }
-    Ok(())
+
+    list
 }
 
-fn lookup_drive_name(
-    param: &Value,
+pub fn extract_drive_name(
+    param: &mut Value,
     config: &SectionConfigData,
 ) -> Result<String, Error> {
 
@@ -100,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)
 }
 
@@ -116,27 +130,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 client = connect_to_localhost()?;
 
-    wait_for_local_worker(result.as_str().unwrap()).await?;
+    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(())
 }
@@ -148,27 +163,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 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(&client, result, &output_format).await?;
 
     Ok(())
 }
@@ -180,25 +196,28 @@ 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 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(())
 }
@@ -210,28 +229,59 @@ async fn eject_media(
                 schema: DRIVE_NAME_SCHEMA,
                 optional: true,
             },
-            "changer-id": {
+            "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,
-    rpcenv: &mut dyn RpcEnvironment,
-) -> Result<(), Error> {
+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::Async(handler) => (handler)(param, info, rpcenv).await?,
-        _ => 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(())
 }
@@ -252,21 +302,16 @@ async fn load_media(
     },
 )]
 /// Load media from the specified slot
-async fn load_media_from_slot(
-    mut param: Value,
-    rpcenv: &mut dyn RpcEnvironment,
-) -> Result<(), Error> {
+async fn load_media_from_slot(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_SLOT;
+    let client = connect_to_localhost()?;
 
-    match info.handler {
-        ApiHandler::Async(handler) => (handler)(param, info, rpcenv).await?,
-        _ => unreachable!(),
-    };
+    let path = format!("api2/json/tape/drive/{}/load-slot", drive);
+    client.put(&path, Some(param)).await?;
 
     Ok(())
 }
@@ -284,25 +329,28 @@ async fn load_media_from_slot(
                 minimum: 1,
                 optional: true,
             },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
         },
     },
 )]
 /// Unload media via changer
-async fn unload_media(
-    mut param: Value,
-    rpcenv: &mut dyn RpcEnvironment,
-) -> Result<(), Error> {
+async fn unload_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_UNLOAD;
+    let drive = extract_drive_name(&mut param, &config)?;
 
-    match info.handler {
-        ApiHandler::Async(handler) => (handler)(param, info, rpcenv).await?,
-        _ => unreachable!(),
-    };
+    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(())
 }
@@ -318,30 +366,31 @@ async fn unload_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 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(&client, result, &output_format).await?;
 
     Ok(())
 }
@@ -353,7 +402,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,
              },
@@ -361,29 +415,30 @@ 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);
@@ -419,42 +474,38 @@ 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 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(&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"))
         ;
 
@@ -474,27 +525,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 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(&client, result, &output_format).await?;
 
     Ok(())
 }
@@ -510,14 +562,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 (config, _digest) = 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 drive = lookup_drive_name(&param, &config)?;
     let mut drive = open_drive(&config, &drive)?;
 
-    drive.move_to_eom()?;
+    drive.move_to_eom(false)?;
 
     Ok(())
 }
@@ -536,11 +592,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) = pbs_config::drive::config()?;
 
-    let (config, _digest) = 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");
@@ -549,12 +609,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() };
@@ -562,24 +629,29 @@ 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:  {}", 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);
-                            }
+                            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");
+                }
             }
         }
     }
@@ -592,30 +664,29 @@ fn debug_scan(param: Value) -> Result<(), Error> {
                 schema: DRIVE_NAME_SCHEMA,
                 optional: true,
             },
-             "output-format": {
+            "output-format": {
                 schema: OUTPUT_FORMAT,
                 optional: true,
-             },
+            },
         },
     },
 )]
 /// Read Cartridge Memory (Medium auxiliary memory attributes)
-fn cartridge_memory(
-    mut param: Value,
-    rpcenv: &mut dyn RpcEnvironment,
-) -> Result<(), Error> {
+async fn cartridge_memory(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 output_format = get_output_format(&param);
-    let info = &api2::tape::drive::API_METHOD_CARTRIDGE_MEMORY;
+    let drive = extract_drive_name(&mut param, &config)?;
 
-    let mut data = match info.handler {
-        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
-        _ => unreachable!(),
-    };
+    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"))
@@ -627,6 +698,44 @@ fn cartridge_memory(
     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: {
@@ -642,27 +751,35 @@ fn cartridge_memory(
     },
 )]
 /// Get drive/media status
-fn status(
-    mut param: Value,
-    rpcenv: &mut dyn RpcEnvironment,
-) -> Result<(), Error> {
+async fn status(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/{}/status", 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_STATUS;
 
-    let mut data = match info.handler {
-        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
-        _ => unreachable!(),
+    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("status"))
+        .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"))
@@ -670,69 +787,173 @@ fn status(
         .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: {
+    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(
-    param: Value,
-    rpcenv: &mut dyn RpcEnvironment,
-) -> Result<(), Error> {
+async fn backup(mut param: Value) -> Result<(), Error> {
 
-    let info = &api2::tape::backup::API_METHOD_BACKUP;
+    let output_format = extract_output_format(&mut param);
 
-    let result = match info.handler {
-        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
-        _ => unreachable!(),
-    };
+    let (config, _digest) = pbs_config::drive::config()?;
+
+    param["drive"] = extract_drive_name(&mut param, &config)?.into();
+
+    let client = connect_to_localhost()?;
 
-    wait_for_local_worker(result.as_str().unwrap()).await?;
+    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_SCHEMA,
+                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(
-    param: Value,
-    rpcenv: &mut dyn RpcEnvironment,
-) -> Result<(), Error> {
+async fn restore(mut param: Value) -> Result<(), Error> {
 
-    let info = &api2::tape::restore::API_METHOD_RESTORE;
+    let output_format = extract_output_format(&mut param);
 
-    let result = match info.handler {
-        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
-        _ => unreachable!(),
-    };
+    let (config, _digest) = pbs_config::drive::config()?;
+
+    param["drive"] = extract_drive_name(&mut param, &config)?.into();
+
+    let client = connect_to_localhost()?;
 
-    wait_for_local_worker(result.as_str().unwrap()).await?;
+    let result = client.post("api2/json/tape/restore", Some(param)).await?;
+
+    view_task_result(&client, result, &output_format).await?;
 
     Ok(())
 }
@@ -749,6 +970,11 @@ async fn restore(
                 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,
@@ -762,23 +988,20 @@ async fn restore(
     },
 )]
 /// Scan media and record content
-async fn catalog_media(
-    mut param: Value,
-    rpcenv: &mut dyn RpcEnvironment,
-)  -> Result<(), Error> {
+async fn catalog_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_CATALOG_MEDIA;
+    let drive = extract_drive_name(&mut param, &config)?;
 
-    let result = match info.handler {
-        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
-        _ => unreachable!(),
-    };
+    let client = connect_to_localhost()?;
+
+    let path = format!("api2/json/tape/drive/{}/catalog", drive);
+    let result = client.post(&path, Some(param)).await?;
 
-    wait_for_local_worker(result.as_str().unwrap()).await?;
+    view_task_result(&client, result, &output_format).await?;
 
     Ok(())
 }
@@ -790,15 +1013,18 @@ fn main() {
             "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"])
+                .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",
@@ -827,8 +1053,8 @@ fn main() {
                 .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(
@@ -856,6 +1082,16 @@ fn main() {
             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)
@@ -867,17 +1103,19 @@ 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("changer-id", complete_media_changer_id)
+                .completion_cb("label-text", complete_media_label_text)
         )
         .insert(
             "load-media-from-slot",
             CliCommand::new(&API_METHOD_LOAD_MEDIA_FROM_SLOT)
-                .arg_param(&["slot"])
+                .arg_param(&["source-slot"])
                 .completion_cb("drive", complete_drive_name)
         )
         .insert(
@@ -885,10 +1123,17 @@ fn main() {
             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("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));
 }