]> git.proxmox.com Git - proxmox-backup.git/blobdiff - src/api2/tape/drive.rs
typo fixes all over the place
[proxmox-backup.git] / src / api2 / tape / drive.rs
index 12c79dd620d6376085881560ae2675d4d3913c06..9e86d8f84f3ac45514ad8ac2c1384660f311dd44 100644 (file)
@@ -1,7 +1,8 @@
+use std::panic::UnwindSafe;
 use std::path::Path;
 use std::sync::Arc;
 
-use anyhow::{bail, Error};
+use anyhow::{bail, format_err, Error};
 use serde_json::Value;
 
 use proxmox::{
@@ -12,27 +13,36 @@ use proxmox::{
     sys::error::SysError,
     api::{
         api,
+        section_config::SectionConfigData,
         RpcEnvironment,
         RpcEnvironmentType,
+        Permission,
         Router,
         SubdirMap,
     },
 };
 
 use crate::{
+    task_log,
     config::{
         self,
-        drive::check_drive_exists,
+        cached_user_info::CachedUserInfo,
+        acl::{
+            PRIV_TAPE_AUDIT,
+            PRIV_TAPE_READ,
+            PRIV_TAPE_WRITE,
+        },
     },
     api2::{
         types::{
             UPID_SCHEMA,
+            CHANGER_NAME_SCHEMA,
             DRIVE_NAME_SCHEMA,
             MEDIA_LABEL_SCHEMA,
             MEDIA_POOL_NAME_SCHEMA,
             Authid,
+            DriveListEntry,
             LinuxTapeDrive,
-            TapeDeviceInfo,
             MediaIdFlat,
             LabelUuidMap,
             MamAttribute,
@@ -43,27 +53,85 @@ use crate::{
     server::WorkerTask,
     tape::{
         TAPE_STATUS_DIR,
-        TapeDriver,
         MediaPool,
         Inventory,
         MediaCatalog,
         MediaId,
         linux_tape_device_list,
-        open_drive,
-        media_changer,
-        required_media_changer,
-        update_changer_online_status,
-        linux_tape::{
-            LinuxTapeHandle,
-            open_linux_tape_device,
-        },
+        lookup_device_identification,
         file_formats::{
             MediaLabel,
             MediaSetLabel,
         },
+        drive::{
+            TapeDriver,
+            LinuxTapeHandle,
+            Lp17VolumeStatistics,
+            open_linux_tape_device,
+            media_changer,
+            required_media_changer,
+            open_drive,
+            lock_tape_device,
+            set_tape_device_state,
+            get_tape_device_state,
+            tape_alert_flags_critical,
+        },
+        changer::update_changer_online_status,
     },
 };
 
+fn run_drive_worker<F>(
+    rpcenv: &dyn RpcEnvironment,
+    drive: String,
+    worker_type: &str,
+    job_id: Option<String>,
+    f: F,
+) -> Result<String, Error>
+where
+    F: Send
+        + UnwindSafe
+        + 'static
+        + FnOnce(Arc<WorkerTask>, SectionConfigData) -> Result<(), Error>,
+{
+    // early check/lock before starting worker
+    let (config, _digest) = config::drive::config()?;
+    let lock_guard = lock_tape_device(&config, &drive)?;
+
+    let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
+    let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
+
+    WorkerTask::new_thread(worker_type, job_id, auth_id, to_stdout, move |worker| {
+        let _lock_guard = lock_guard;
+        set_tape_device_state(&drive, &worker.upid().to_string())
+            .map_err(|err| format_err!("could not set tape device state: {}", err))?;
+
+        let result = f(worker, config);
+        set_tape_device_state(&drive, "")
+            .map_err(|err| format_err!("could not unset tape device state: {}", err))?;
+        result
+    })
+}
+
+async fn run_drive_blocking_task<F, R>(drive: String, state: String, f: F) -> Result<R, Error>
+where
+    F: Send + 'static + FnOnce(SectionConfigData) -> Result<R, Error>,
+    R: Send + 'static,
+{
+    // early check/lock before starting worker
+    let (config, _digest) = config::drive::config()?;
+    let lock_guard = lock_tape_device(&config, &drive)?;
+    tokio::task::spawn_blocking(move || {
+        let _lock_guard = lock_guard;
+        set_tape_device_state(&drive, &state)
+            .map_err(|err| format_err!("could not set tape device state: {}", err))?;
+        let result = f(config);
+        set_tape_device_state(&drive, "")
+            .map_err(|err| format_err!("could not unset tape device state: {}", err))?;
+        result
+    })
+    .await?
+}
+
 #[api(
     input: {
         properties: {
@@ -75,18 +143,37 @@ use crate::{
             },
         },
     },
+    returns: {
+        schema: UPID_SCHEMA,
+    },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
+    },
 )]
 /// Load media with specified label
 ///
 /// Issue a media load request to the associated changer device.
-pub async fn load_media(drive: String, label_text: String) -> Result<(), Error> {
-
-    let (config, _digest) = config::drive::config()?;
+pub fn load_media(
+    drive: String,
+    label_text: String,
+    rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Value, Error> {
+    let job_id = format!("{}:{}", drive, label_text);
+
+    let upid_str = run_drive_worker(
+        rpcenv,
+        drive.clone(),
+        "load-media",
+        Some(job_id),
+        move |worker, config| {
+            task_log!(worker, "loading media '{}' into drive '{}'", label_text, drive);
+            let (mut changer, _) = required_media_changer(&config, &drive)?;
+            changer.load_media(&label_text)?;
+            Ok(())
+        },
+    )?;
 
-    tokio::task::spawn_blocking(move || {
-        let (mut changer, _) = required_media_changer(&config, &drive)?;
-        changer.load_media(&label_text)
-    }).await?
+    Ok(upid_str.into())
 }
 
 #[api(
@@ -101,18 +188,24 @@ pub async fn load_media(drive: String, label_text: String) -> Result<(), Error>
             },
         },
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
+    },
 )]
 /// Load media from the specified slot
 ///
 /// Issue a media load request to the associated changer device.
 pub async fn load_slot(drive: String, source_slot: u64) -> Result<(), Error> {
-
-    let (config, _digest) = config::drive::config()?;
-
-    tokio::task::spawn_blocking(move || {
-        let (mut changer, _) = required_media_changer(&config, &drive)?;
-        changer.load_media_from_slot(source_slot)
-    }).await?
+    run_drive_blocking_task(
+        drive.clone(),
+        format!("load from slot {}", source_slot),
+        move |config| {
+            let (mut changer, _) = required_media_changer(&config, &drive)?;
+            changer.load_media_from_slot(source_slot)?;
+            Ok(())
+        },
+    )
+    .await
 }
 
 #[api(
@@ -127,23 +220,32 @@ pub async fn load_slot(drive: String, source_slot: u64) -> Result<(), Error> {
         },
     },
     returns: {
-        description: "The import-export slot number the media was transfered to.",
+        description: "The import-export slot number the media was transferred to.",
         type: u64,
         minimum: 1,
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
+    },
 )]
 /// Export media with specified label
 pub async fn export_media(drive: String, label_text: String) -> Result<u64, Error> {
-
-    let (config, _digest) = config::drive::config()?;
-
-    tokio::task::spawn_blocking(move || {
-        let (mut changer, changer_name) = required_media_changer(&config, &drive)?;
-        match changer.export_media(&label_text)? {
-            Some(slot) => Ok(slot),
-            None => bail!("media '{}' is not online (via changer '{}')", label_text, changer_name),
+    run_drive_blocking_task(
+        drive.clone(),
+        format!("export media {}", label_text),
+        move |config| {
+            let (mut changer, changer_name) = required_media_changer(&config, &drive)?;
+            match changer.export_media(&label_text)? {
+                Some(slot) => Ok(slot),
+                None => bail!(
+                    "media '{}' is not online (via changer '{}')",
+                    label_text,
+                    changer_name
+                ),
+            }
         }
-    }).await?
+    )
+    .await
 }
 
 #[api(
@@ -159,40 +261,34 @@ pub async fn export_media(drive: String, label_text: String) -> Result<u64, Erro
             },
         },
     },
+    returns: {
+        schema: UPID_SCHEMA,
+    },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
+    },
 )]
 /// Unload media via changer
-pub async fn unload(
+pub fn unload(
     drive: String,
     target_slot: Option<u64>,
-    _param: Value,
-) -> Result<(), Error> {
-
-    let (config, _digest) = config::drive::config()?;
-
-    tokio::task::spawn_blocking(move || {
-        let (mut changer, _) = required_media_changer(&config, &drive)?;
-        changer.unload_media(target_slot)
-    }).await?
-}
+    rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Value, Error> {
+    let upid_str = run_drive_worker(
+        rpcenv,
+        drive.clone(),
+        "unload-media",
+        Some(drive.clone()),
+        move |worker, config| {
+            task_log!(worker, "unloading media from drive '{}'", drive);
 
-#[api(
-    input: {
-        properties: {},
-    },
-    returns: {
-        description: "The list of autodetected tape drives.",
-        type: Array,
-        items: {
-            type: TapeDeviceInfo,
+            let (mut changer, _) = required_media_changer(&config, &drive)?;
+            changer.unload_media(target_slot)?;
+            Ok(())
         },
-    },
-)]
-/// Scan tape drives
-pub fn scan_drives(_param: Value) -> Result<Vec<TapeDeviceInfo>, Error> {
-
-    let list = linux_tape_device_list();
+    )?;
 
-    Ok(list)
+    Ok(upid_str.into())
 }
 
 #[api(
@@ -207,37 +303,86 @@ pub fn scan_drives(_param: Value) -> Result<Vec<TapeDeviceInfo>, Error> {
                 optional: true,
                 default: true,
             },
+            "label-text": {
+                schema: MEDIA_LABEL_SCHEMA,
+                optional: true,
+            },
         },
     },
     returns: {
         schema: UPID_SCHEMA,
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE, false),
+    },
 )]
-/// Erase media
+/// Erase media. Check for label-text if given (cancels if wrong media).
 pub fn erase_media(
     drive: String,
     fast: Option<bool>,
+    label_text: Option<String>,
     rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<Value, Error> {
+    let upid_str = run_drive_worker(
+        rpcenv,
+        drive.clone(),
+        "erase-media",
+        Some(drive.clone()),
+        move |worker, config| {
+            if let Some(ref label) = label_text {
+                task_log!(worker, "try to load media '{}'", label);
+                if let Some((mut changer, _)) = media_changer(&config, &drive)? {
+                    changer.load_media(label)?;
+                }
+            }
 
-    let (config, _digest) = config::drive::config()?;
+            let mut handle = open_drive(&config, &drive)?;
 
-    check_drive_exists(&config, &drive)?; // early check before starting worker
+            match handle.read_label() {
+                Err(err) => {
+                    if let Some(label) = label_text {
+                        bail!("expected label '{}', found unrelated data", label);
+                    }
+                    /* assume drive contains no or unrelated data */
+                    task_log!(worker, "unable to read media label: {}", err);
+                    task_log!(worker, "erase anyways");
+                    handle.erase_media(fast.unwrap_or(true))?;
+                }
+                Ok((None, _)) => {
+                    if let Some(label) = label_text {
+                        bail!("expected label '{}', found empty tape", label);
+                    }
+                    task_log!(worker, "found empty media - erase anyways");
+                    handle.erase_media(fast.unwrap_or(true))?;
+                }
+                Ok((Some(media_id), _key_config)) => {
+                    if let Some(label_text) = label_text {
+                        if media_id.label.label_text != label_text {
+                            bail!(
+                                "expected label '{}', found '{}', aborting",
+                                label_text,
+                                media_id.label.label_text
+                            );
+                        }
+                    }
 
-    let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
+                    task_log!(
+                        worker,
+                        "found media '{}' with uuid '{}'",
+                        media_id.label.label_text, media_id.label.uuid,
+                    );
 
-    let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
+                    let status_path = Path::new(TAPE_STATUS_DIR);
+                    let mut inventory = Inventory::load(status_path)?;
+
+                    MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
+                    inventory.remove_media(&media_id.label.uuid)?;
+                    handle.erase_media(fast.unwrap_or(true))?;
+                }
+            }
 
-    let upid_str = WorkerTask::new_thread(
-        "erase-media",
-        Some(drive.clone()),
-        auth_id,
-        to_stdout,
-        move |_worker| {
-            let mut drive = open_drive(&config, &drive)?;
-            drive.erase_media(fast.unwrap_or(true))?;
             Ok(())
-        }
+        },
     )?;
 
     Ok(upid_str.into())
@@ -254,31 +399,25 @@ pub fn erase_media(
     returns: {
         schema: UPID_SCHEMA,
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
+    },
 )]
 /// Rewind tape
 pub fn rewind(
     drive: String,
     rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<Value, Error> {
-
-    let (config, _digest) = config::drive::config()?;
-
-    check_drive_exists(&config, &drive)?; // early check before starting worker
-
-    let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
-
-    let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
-
-    let upid_str = WorkerTask::new_thread(
+    let upid_str = run_drive_worker(
+        rpcenv,
+        drive.clone(),
         "rewind-media",
         Some(drive.clone()),
-        auth_id,
-        to_stdout,
-        move |_worker| {
+        move |_worker, config| {
             let mut drive = open_drive(&config, &drive)?;
             drive.rewind()?;
             Ok(())
-        }
+        },
     )?;
 
     Ok(upid_str.into())
@@ -292,21 +431,35 @@ pub fn rewind(
             },
         },
     },
+    returns: {
+        schema: UPID_SCHEMA,
+    },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
+    },
 )]
 /// Eject/Unload drive media
-pub async fn eject_media(drive: String) -> Result<(), Error> {
-
-    let (config, _digest) = config::drive::config()?;
+pub fn eject_media(
+    drive: String,
+    rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Value, Error> {
+    let upid_str = run_drive_worker(
+        rpcenv,
+        drive.clone(),
+        "eject-media",
+        Some(drive.clone()),
+        move |_worker, config| {
+            if let Some((mut changer, _)) = media_changer(&config, &drive)? {
+                changer.unload_media(None)?;
+            } else {
+                let mut drive = open_drive(&config, &drive)?;
+                drive.eject_media()?;
+            }
+            Ok(())
+        },
+    )?;
 
-    tokio::task::spawn_blocking(move || {
-        if let Some((mut changer, _)) = media_changer(&config, &drive)? {
-            changer.unload_media(None)?;
-        } else {
-            let mut drive = open_drive(&config, &drive)?;
-            drive.eject_media()?;
-        }
-        Ok(())
-    }).await?
+    Ok(upid_str.into())
 }
 
 #[api(
@@ -327,6 +480,9 @@ pub async fn eject_media(drive: String) -> Result<(), Error> {
     returns: {
         schema: UPID_SCHEMA,
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE, false),
+    },
 )]
 /// Label media
 ///
@@ -340,9 +496,6 @@ pub fn label_media(
     label_text: String,
     rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<Value, Error> {
-
-    let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
-
     if let Some(ref pool) = pool {
         let (pool_config, _digest) = config::media_pool::config()?;
 
@@ -350,18 +503,12 @@ pub fn label_media(
             bail!("no such pool ('{}')", pool);
         }
     }
-
-    let (config, _digest) = config::drive::config()?;
-
-    let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
-
-    let upid_str = WorkerTask::new_thread(
+    let upid_str = run_drive_worker(
+        rpcenv,
+        drive.clone(),
         "label-media",
         Some(drive.clone()),
-        auth_id,
-        to_stdout,
-        move |worker| {
-
+        move |worker, config| {
             let mut drive = open_drive(&config, &drive)?;
 
             drive.rewind()?;
@@ -386,7 +533,7 @@ pub fn label_media(
             };
 
             write_media_label(worker, &mut drive, label, pool)
-        }
+        },
     )?;
 
     Ok(upid_str.into())
@@ -408,7 +555,7 @@ fn write_media_label(
         worker.log(format!("Label media '{}' for pool '{}'", label.label_text, pool));
         let set = MediaSetLabel::with_data(&pool, [0u8; 16].into(), 0, label.ctime, None);
 
-        drive.write_media_set_label(&set)?;
+        drive.write_media_set_label(&set, None)?;
         media_set_label = Some(set);
     } else {
         worker.log(format!("Label media '{}' (no pool assignment)", label.label_text));
@@ -427,7 +574,7 @@ fn write_media_label(
     drive.rewind()?;
 
     match drive.read_label() {
-        Ok(Some(info)) => {
+        Ok((Some(info), _)) => {
             if info.label.uuid != media_id.label.uuid {
                 bail!("verify label failed - got wrong label uuid");
             }
@@ -447,7 +594,7 @@ fn write_media_label(
                 }
             }
         },
-        Ok(None) => bail!("verify label failed (got empty media)"),
+        Ok((None, _)) => bail!("verify label failed (got empty media)"),
         Err(err) => bail!("verify label failed - {}", err),
     };
 
@@ -457,6 +604,49 @@ fn write_media_label(
 }
 
 #[api(
+    protected: true,
+    input: {
+        properties: {
+            drive: {
+                schema: DRIVE_NAME_SCHEMA,
+            },
+            password: {
+                description: "Encryption key password.",
+            },
+        },
+    },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
+    },
+)]
+/// Try to restore a tape encryption key
+pub async fn restore_key(
+    drive: String,
+    password: String,
+) -> Result<(), Error> {
+    run_drive_blocking_task(
+        drive.clone(),
+        "restore key".to_string(),
+        move |config| {
+            let mut drive = open_drive(&config, &drive)?;
+
+            let (_media_id, key_config) = drive.read_label()?;
+
+            if let Some(key_config) = key_config {
+                let password_fn = || { Ok(password.as_bytes().to_vec()) };
+                let (key, ..) = key_config.decrypt(&password_fn)?;
+                config::tape_encryption_keys::insert_key(key, key_config, true)?;
+            } else {
+                bail!("media does not contain any encryption key configuration");
+            }
+
+            Ok(())
+        }
+    )
+    .await
+}
+
+ #[api(
     input: {
         properties: {
             drive: {
@@ -471,58 +661,71 @@ fn write_media_label(
     returns: {
         type: MediaIdFlat,
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
+    },
 )]
-/// Read media label
+/// Read media label (optionally inventorize media)
 pub async fn read_label(
     drive: String,
     inventorize: Option<bool>,
 ) -> Result<MediaIdFlat, Error> {
+    run_drive_blocking_task(
+        drive.clone(),
+        "reading label".to_string(),
+        move |config| {
+            let mut drive = open_drive(&config, &drive)?;
 
-    let (config, _digest) = config::drive::config()?;
+            let (media_id, _key_config) = drive.read_label()?;
 
-    tokio::task::spawn_blocking(move || {
-        let mut drive = open_drive(&config, &drive)?;
-
-        let media_id = drive.read_label()?;
-
-        let media_id = match media_id {
-            Some(media_id) => {
-                let mut flat = MediaIdFlat {
-                    uuid: media_id.label.uuid.to_string(),
-                    label_text: media_id.label.label_text.clone(),
-                    ctime: media_id.label.ctime,
-                    media_set_ctime: None,
-                    media_set_uuid: None,
-                    encryption_key_fingerprint: None,
-                    pool: None,
-                    seq_nr: None,
-                };
-                if let Some(ref set) = media_id.media_set_label {
-                    flat.pool = Some(set.pool.clone());
-                    flat.seq_nr = Some(set.seq_nr);
-                    flat.media_set_uuid = Some(set.uuid.to_string());
-                    flat.media_set_ctime = Some(set.ctime);
-                    flat.encryption_key_fingerprint = set
-                        .encryption_key_fingerprint
-                        .as_ref()
-                        .map(|fp| crate::tools::format::as_fingerprint(fp.bytes()));
-                }
+            let media_id = match media_id {
+                Some(media_id) => {
+                    let mut flat = MediaIdFlat {
+                        uuid: media_id.label.uuid.clone(),
+                        label_text: media_id.label.label_text.clone(),
+                        ctime: media_id.label.ctime,
+                        media_set_ctime: None,
+                        media_set_uuid: None,
+                        encryption_key_fingerprint: None,
+                        pool: None,
+                        seq_nr: None,
+                    };
+                    if let Some(ref set) = media_id.media_set_label {
+                        flat.pool = Some(set.pool.clone());
+                        flat.seq_nr = Some(set.seq_nr);
+                        flat.media_set_uuid = Some(set.uuid.clone());
+                        flat.media_set_ctime = Some(set.ctime);
+                        flat.encryption_key_fingerprint = set
+                            .encryption_key_fingerprint
+                            .as_ref()
+                            .map(|fp| crate::tools::format::as_fingerprint(fp.bytes()));
+
+                        let encrypt_fingerprint = set.encryption_key_fingerprint.clone()
+                            .map(|fp| (fp, set.uuid.clone()));
+
+                        if let Err(err) = drive.set_encryption(encrypt_fingerprint) {
+                            // try, but ignore errors. just log to stderr
+                            eprintln!("uable to load encryption key: {}", err);
+                        }
+                    }
 
-                if let Some(true) = inventorize {
-                    let state_path = Path::new(TAPE_STATUS_DIR);
-                    let mut inventory = Inventory::load(state_path)?;
-                    inventory.store(media_id, false)?;
-                }
+                    if let Some(true) = inventorize {
+                        let state_path = Path::new(TAPE_STATUS_DIR);
+                        let mut inventory = Inventory::load(state_path)?;
+                        inventory.store(media_id, false)?;
+                    }
 
-                flat
-            }
-            None => {
-                bail!("Media is empty (no label).");
-            }
-        };
+                    flat
+                }
+                None => {
+                    bail!("Media is empty (no label).");
+                }
+            };
 
-        Ok(media_id)
-    }).await?
+            Ok(media_id)
+        }
+    )
+    .await
 }
 
 #[api(
@@ -536,38 +739,54 @@ pub async fn read_label(
     returns: {
         schema: UPID_SCHEMA,
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
+    },
 )]
 /// Clean drive
 pub fn clean_drive(
     drive: String,
     rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<Value, Error> {
-
-    let (config, _digest) = config::drive::config()?;
-
-    check_drive_exists(&config, &drive)?; // early check before starting worker
-
-    let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
-
-    let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
-
-    let upid_str = WorkerTask::new_thread(
+    let upid_str = run_drive_worker(
+        rpcenv,
+        drive.clone(),
         "clean-drive",
         Some(drive.clone()),
-        auth_id,
-        to_stdout,
-        move |worker| {
-
+        move |worker, config| {
             let (mut changer, _changer_name) = required_media_changer(&config, &drive)?;
 
             worker.log("Starting drive clean");
 
             changer.clean_drive()?;
 
-            worker.log("Drive cleaned sucessfully");
+             if let Ok(drive_config) = config.lookup::<LinuxTapeDrive>("linux", &drive) {
+                 // Note: clean_drive unloads the cleaning media, so we cannot use drive_config.open
+                 let mut handle = LinuxTapeHandle::new(open_linux_tape_device(&drive_config.path)?);
+
+                 // test for critical tape alert flags
+                 if let Ok(alert_flags) = handle.tape_alert_flags() {
+                     if !alert_flags.is_empty() {
+                         worker.log(format!("TapeAlertFlags: {:?}", alert_flags));
+                         if tape_alert_flags_critical(alert_flags) {
+                             bail!("found critical tape alert flags: {:?}", alert_flags);
+                         }
+                     }
+                 }
+
+                 // test wearout (max. 50 mounts)
+                 if let Ok(volume_stats) = handle.volume_statistics() {
+                     worker.log(format!("Volume mounts: {}", volume_stats.volume_mounts));
+                     let wearout = volume_stats.volume_mounts * 2; // (*100.0/50.0);
+                     worker.log(format!("Cleaning tape wearout: {}%", wearout));
+                 }
+             }
+
+            worker.log("Drive cleaned successfully");
 
             Ok(())
-        })?;
+        },
+    )?;
 
     Ok(upid_str.into())
 }
@@ -587,6 +806,9 @@ pub fn clean_drive(
             type: LabelUuidMap,
         },
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
+    },
 )]
 /// List known media labels (Changer Inventory)
 ///
@@ -598,44 +820,46 @@ pub fn clean_drive(
 pub async fn inventory(
     drive: String,
 ) -> Result<Vec<LabelUuidMap>, Error> {
+    run_drive_blocking_task(
+        drive.clone(),
+        "inventorize".to_string(),
+        move |config| {
+            let (mut changer, changer_name) = required_media_changer(&config, &drive)?;
 
-    let (config, _digest) = config::drive::config()?;
+            let label_text_list = changer.online_media_label_texts()?;
 
-    tokio::task::spawn_blocking(move || {
-        let (mut changer, changer_name) = required_media_changer(&config, &drive)?;
+            let state_path = Path::new(TAPE_STATUS_DIR);
 
-        let label_text_list = changer.online_media_label_texts()?;
+            let mut inventory = Inventory::load(state_path)?;
 
-        let state_path = Path::new(TAPE_STATUS_DIR);
+            update_changer_online_status(
+                &config,
+                &mut inventory,
+                &changer_name,
+                &label_text_list,
+            )?;
 
-        let mut inventory = Inventory::load(state_path)?;
+            let mut list = Vec::new();
 
-        update_changer_online_status(
-            &config,
-            &mut inventory,
-            &changer_name,
-            &label_text_list,
-        )?;
+            for label_text in label_text_list.iter() {
+                if label_text.starts_with("CLN") {
+                    // skip cleaning unit
+                    continue;
+                }
 
-        let mut list = Vec::new();
+                let label_text = label_text.to_string();
 
-        for label_text in label_text_list.iter() {
-            if label_text.starts_with("CLN") {
-                // skip cleaning unit
-                continue;
+                if let Some(media_id) = inventory.find_media_by_label_text(&label_text) {
+                    list.push(LabelUuidMap { label_text, uuid: Some(media_id.label.uuid.clone()) });
+                } else {
+                    list.push(LabelUuidMap { label_text, uuid: None });
+                }
             }
 
-            let label_text = label_text.to_string();
-
-            if let Some(media_id) = inventory.find_media_by_label_text(&label_text) {
-                list.push(LabelUuidMap { label_text, uuid: Some(media_id.label.uuid.to_string()) });
-            } else {
-                list.push(LabelUuidMap { label_text, uuid: None });
-            }
+            Ok(list)
         }
-
-        Ok(list)
-    }).await?
+    )
+    .await
 }
 
 #[api(
@@ -654,6 +878,9 @@ pub async fn inventory(
     returns: {
         schema: UPID_SCHEMA,
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
+    },
 )]
 /// Update inventory
 ///
@@ -669,27 +896,17 @@ pub fn update_inventory(
     read_all_labels: Option<bool>,
     rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<Value, Error> {
-
-    let (config, _digest) = config::drive::config()?;
-
-    check_drive_exists(&config, &drive)?; // early check before starting worker
-
-    let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
-
-    let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
-
-    let upid_str = WorkerTask::new_thread(
+    let upid_str = run_drive_worker(
+        rpcenv,
+        drive.clone(),
         "inventory-update",
         Some(drive.clone()),
-        auth_id,
-        to_stdout,
-        move |worker| {
-
+        move |worker, config| {
             let (mut changer, changer_name) = required_media_changer(&config, &drive)?;
 
             let label_text_list = changer.online_media_label_texts()?;
             if label_text_list.is_empty() {
-                worker.log(format!("changer device does not list any media labels"));
+                worker.log("changer device does not list any media labels".to_string());
             }
 
             let state_path = Path::new(TAPE_STATUS_DIR);
@@ -706,11 +923,9 @@ pub fn update_inventory(
 
                 let label_text = label_text.to_string();
 
-                if !read_all_labels.unwrap_or(false) {
-                    if let Some(_) = inventory.find_media_by_label_text(&label_text) {
-                        worker.log(format!("media '{}' already inventoried", label_text));
-                        continue;
-                    }
+                if !read_all_labels.unwrap_or(false) && inventory.find_media_by_label_text(&label_text).is_some() {
+                    worker.log(format!("media '{}' already inventoried", label_text));
+                    continue;
                 }
 
                 if let Err(err) = changer.load_media(&label_text) {
@@ -723,12 +938,12 @@ pub fn update_inventory(
                     Err(err) => {
                         worker.warn(format!("unable to read label form media '{}' - {}", label_text, err));
                     }
-                    Ok(None) => {
+                    Ok((None, _)) => {
                         worker.log(format!("media '{}' is empty", label_text));
                     }
-                    Ok(Some(media_id)) => {
+                    Ok((Some(media_id), _key_config)) => {
                         if label_text != media_id.label.label_text {
-                            worker.warn(format!("label text missmatch ({} != {})", label_text, media_id.label.label_text));
+                            worker.warn(format!("label text mismatch ({} != {})", label_text, media_id.label.label_text));
                             continue;
                         }
                         worker.log(format!("inventorize media '{}' with uuid '{}'", label_text, media_id.label.uuid));
@@ -738,7 +953,7 @@ pub fn update_inventory(
                 changer.unload_media(None)?;
             }
             Ok(())
-        }
+        },
     )?;
 
     Ok(upid_str.into())
@@ -760,6 +975,9 @@ pub fn update_inventory(
     returns: {
         schema: UPID_SCHEMA,
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE, false),
+    },
 )]
 /// Label media with barcodes from changer device
 pub fn barcode_label_media(
@@ -767,7 +985,6 @@ pub fn barcode_label_media(
     pool: Option<String>,
     rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<Value, Error> {
-
     if let Some(ref pool) = pool {
         let (pool_config, _digest) = config::media_pool::config()?;
 
@@ -776,18 +993,12 @@ pub fn barcode_label_media(
         }
     }
 
-    let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
-
-    let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
-
-    let upid_str = WorkerTask::new_thread(
+    let upid_str = run_drive_worker(
+        rpcenv,
+        drive.clone(),
         "barcode-label-media",
         Some(drive.clone()),
-        auth_id,
-        to_stdout,
-        move |worker| {
-            barcode_label_media_worker(worker, drive, pool)
-        }
+        move |worker, config| barcode_label_media_worker(worker, drive, &config, pool),
     )?;
 
     Ok(upid_str.into())
@@ -796,20 +1007,21 @@ pub fn barcode_label_media(
 fn barcode_label_media_worker(
     worker: Arc<WorkerTask>,
     drive: String,
+    drive_config: &SectionConfigData,
     pool: Option<String>,
 ) -> Result<(), Error> {
+    let (mut changer, changer_name) = required_media_changer(drive_config, &drive)?;
 
-    let (config, _digest) = config::drive::config()?;
-
-    let (mut changer, changer_name) = required_media_changer(&config, &drive)?;
+    let mut label_text_list = changer.online_media_label_texts()?;
 
-    let label_text_list = changer.online_media_label_texts()?;
+    // make sure we label them in the right order
+    label_text_list.sort();
 
     let state_path = Path::new(TAPE_STATUS_DIR);
 
     let mut inventory = Inventory::load(state_path)?;
 
-    update_changer_online_status(&config, &mut inventory, &changer_name, &label_text_list)?;
+    update_changer_online_status(drive_config, &mut inventory, &changer_name, &label_text_list)?;
 
     if label_text_list.is_empty() {
         bail!("changer device does not list any media labels");
@@ -831,7 +1043,7 @@ fn barcode_label_media_worker(
             continue;
         }
 
-        let mut drive = open_drive(&config, &drive)?;
+        let mut drive = open_drive(drive_config, &drive)?;
         drive.rewind()?;
 
         match drive.read_next_file() {
@@ -878,16 +1090,53 @@ fn barcode_label_media_worker(
             type: MamAttribute,
         },
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT, false),
+    },
 )]
 /// Read Cartridge Memory (Medium auxiliary memory attributes)
-pub fn cartridge_memory(drive: String) -> Result<Vec<MamAttribute>, Error> {
-
-    let (config, _digest) = config::drive::config()?;
-
-    let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
-    let mut handle = drive_config.open()?;
+pub async fn cartridge_memory(drive: String) -> Result<Vec<MamAttribute>, Error> {
+    run_drive_blocking_task(
+        drive.clone(),
+        "reading cartridge memory".to_string(),
+        move |config| {
+            let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
+            let mut handle = drive_config.open()?;
+
+            handle.cartridge_memory()
+        }
+    )
+    .await
+}
 
-    handle.cartridge_memory()
+#[api(
+    input: {
+        properties: {
+            drive: {
+                schema: DRIVE_NAME_SCHEMA,
+            },
+        },
+    },
+    returns: {
+        type: Lp17VolumeStatistics,
+    },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT, false),
+    },
+)]
+/// Read Volume Statistics (SCSI log page 17h)
+pub async fn volume_statistics(drive: String) -> Result<Lp17VolumeStatistics, Error> {
+    run_drive_blocking_task(
+        drive.clone(),
+        "reading volume statistics".to_string(),
+        move |config| {
+            let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
+            let mut handle = drive_config.open()?;
+
+            handle.volume_statistics()
+        }
+    )
+    .await
 }
 
 #[api(
@@ -901,20 +1150,27 @@ pub fn cartridge_memory(drive: String) -> Result<Vec<MamAttribute>, Error> {
     returns: {
         type: LinuxDriveAndMediaStatus,
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT, false),
+    },
 )]
 /// Get drive/media status
-pub fn status(drive: String) -> Result<LinuxDriveAndMediaStatus, Error> {
-
-    let (config, _digest) = config::drive::config()?;
+pub async fn status(drive: String) -> Result<LinuxDriveAndMediaStatus, Error> {
+    run_drive_blocking_task(
+        drive.clone(),
+        "reading drive status".to_string(),
+        move |config| {
+            let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
 
-    let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
+            // Note: use open_linux_tape_device, because this also works if no medium loaded
+            let file = open_linux_tape_device(&drive_config.path)?;
 
-    // Note: use open_linux_tape_device, because this also works if no medium loaded
-    let file = open_linux_tape_device(&drive_config.path)?;
+            let mut handle = LinuxTapeHandle::new(file);
 
-    let mut handle = LinuxTapeHandle::new(file);
-
-    handle.get_drive_and_media_status()
+            handle.get_drive_and_media_status()
+        }
+    )
+    .await
 }
 
 #[api(
@@ -938,6 +1194,9 @@ pub fn status(drive: String) -> Result<LinuxDriveAndMediaStatus, Error> {
     returns: {
         schema: UPID_SCHEMA,
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
+    },
 )]
 /// Scan media and record content
 pub fn catalog_media(
@@ -946,38 +1205,34 @@ pub fn catalog_media(
     verbose: Option<bool>,
     rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<Value, Error> {
-
     let verbose = verbose.unwrap_or(false);
     let force = force.unwrap_or(false);
 
-    let (config, _digest) = config::drive::config()?;
-
-    check_drive_exists(&config, &drive)?; // early check before starting worker
-
-    let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
-
-    let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
-
-    let upid_str = WorkerTask::new_thread(
+    let upid_str = run_drive_worker(
+        rpcenv,
+        drive.clone(),
         "catalog-media",
         Some(drive.clone()),
-        auth_id,
-        to_stdout,
-        move |worker| {
-
+        move |worker, config| {
             let mut drive = open_drive(&config, &drive)?;
 
             drive.rewind()?;
 
             let media_id = match drive.read_label()? {
-                Some(media_id) => {
+                (Some(media_id), key_config) => {
                     worker.log(format!(
                         "found media label: {}",
                         serde_json::to_string_pretty(&serde_json::to_value(&media_id)?)?
                     ));
+                    if key_config.is_some() {
+                        worker.log(format!(
+                            "encryption key config: {}",
+                            serde_json::to_string_pretty(&serde_json::to_value(&key_config)?)?
+                        ));
+                    }
                     media_id
                 },
-                None => bail!("media is empty (no media label found)"),
+                (None, _) => bail!("media is empty (no media label found)"),
             };
 
             let status_path = Path::new(TAPE_STATUS_DIR);
@@ -997,7 +1252,9 @@ pub fn catalog_media(
                         MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
                         return Ok(());
                     }
-                    let encrypt_fingerprint = set.encryption_key_fingerprint.clone();
+                    let encrypt_fingerprint = set.encryption_key_fingerprint.clone()
+                        .map(|fp| (fp, set.uuid.clone()));
+
                     drive.set_encryption(encrypt_fingerprint)?;
 
                     set.pool.clone()
@@ -1006,33 +1263,87 @@ pub fn catalog_media(
 
             let _lock = MediaPool::lock(status_path, &pool)?;
 
-            if MediaCatalog::exists(status_path, &media_id.label.uuid) {
-                if !force {
-                    bail!("media catalog exists (please use --force to overwrite)");
-                }
+            if MediaCatalog::exists(status_path, &media_id.label.uuid) && !force {
+                bail!("media catalog exists (please use --force to overwrite)");
             }
 
             restore_media(&worker, &mut drive, &media_id, None, verbose)?;
 
             Ok(())
-
-        }
+        },
     )?;
 
     Ok(upid_str.into())
 }
 
+#[api(
+    input: {
+        properties: {
+            changer: {
+                schema: CHANGER_NAME_SCHEMA,
+                optional: true,
+            },
+        },
+    },
+    returns: {
+        description: "The list of configured drives with model information.",
+        type: Array,
+        items: {
+            type: DriveListEntry,
+        },
+    },
+    access: {
+        description: "List configured tape drives filtered by Tape.Audit privileges",
+        permission: &Permission::Anybody,
+    },
+)]
+/// List drives
+pub fn list_drives(
+    changer: Option<String>,
+    _param: Value,
+    rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Vec<DriveListEntry>, Error> {
+    let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
+    let user_info = CachedUserInfo::new()?;
+
+    let (config, _) = config::drive::config()?;
+
+    let linux_drives = linux_tape_device_list();
+
+    let drive_list: Vec<LinuxTapeDrive> = config.convert_to_typed_array("linux")?;
+
+    let mut list = Vec::new();
+
+    for drive in drive_list {
+        if changer.is_some() && drive.changer != changer {
+            continue;
+        }
+
+        let privs = user_info.lookup_privs(&auth_id, &["tape", "drive", &drive.name]);
+        if (privs & PRIV_TAPE_AUDIT) == 0 {
+            continue;
+        }
+
+        let info = lookup_device_identification(&linux_drives, &drive.path);
+        let state = get_tape_device_state(&config, &drive.name)?;
+        let entry = DriveListEntry { config: drive, info, state };
+        list.push(entry);
+    }
+
+    Ok(list)
+}
+
 #[sortable]
 pub const SUBDIRS: SubdirMap = &sorted!([
     (
         "barcode-label-media",
         &Router::new()
-            .put(&API_METHOD_BARCODE_LABEL_MEDIA)
+            .post(&API_METHOD_BARCODE_LABEL_MEDIA)
     ),
     (
         "catalog",
         &Router::new()
-            .put(&API_METHOD_CATALOG_MEDIA)
+            .post(&API_METHOD_CATALOG_MEDIA)
     ),
     (
         "clean",
@@ -1042,12 +1353,17 @@ pub const SUBDIRS: SubdirMap = &sorted!([
     (
         "eject-media",
         &Router::new()
-            .put(&API_METHOD_EJECT_MEDIA)
+            .post(&API_METHOD_EJECT_MEDIA)
     ),
     (
         "erase-media",
         &Router::new()
-            .put(&API_METHOD_ERASE_MEDIA)
+            .post(&API_METHOD_ERASE_MEDIA)
+    ),
+    (
+        "export-media",
+        &Router::new()
+            .put(&API_METHOD_EXPORT_MEDIA)
     ),
     (
         "inventory",
@@ -1058,7 +1374,12 @@ pub const SUBDIRS: SubdirMap = &sorted!([
     (
         "label-media",
         &Router::new()
-            .put(&API_METHOD_LABEL_MEDIA)
+            .post(&API_METHOD_LABEL_MEDIA)
+    ),
+    (
+        "load-media",
+        &Router::new()
+            .post(&API_METHOD_LOAD_MEDIA)
     ),
     (
         "load-slot",
@@ -1068,7 +1389,12 @@ pub const SUBDIRS: SubdirMap = &sorted!([
     (
         "cartridge-memory",
         &Router::new()
-            .put(&API_METHOD_CARTRIDGE_MEMORY)
+            .get(&API_METHOD_CARTRIDGE_MEMORY)
+    ),
+    (
+        "volume-statistics",
+        &Router::new()
+            .get(&API_METHOD_VOLUME_STATISTICS)
     ),
     (
         "read-label",
@@ -1076,14 +1402,14 @@ pub const SUBDIRS: SubdirMap = &sorted!([
             .get(&API_METHOD_READ_LABEL)
     ),
     (
-        "rewind",
+        "restore-key",
         &Router::new()
-            .put(&API_METHOD_REWIND)
+            .post(&API_METHOD_RESTORE_KEY)
     ),
     (
-        "scan",
+        "rewind",
         &Router::new()
-            .get(&API_METHOD_SCAN_DRIVES)
+            .post(&API_METHOD_REWIND)
     ),
     (
         "status",
@@ -1093,10 +1419,14 @@ pub const SUBDIRS: SubdirMap = &sorted!([
     (
         "unload",
         &Router::new()
-            .put(&API_METHOD_UNLOAD)
+            .post(&API_METHOD_UNLOAD)
     ),
 ]);
 
-pub const ROUTER: Router = Router::new()
+const ITEM_ROUTER: Router = Router::new()
     .get(&list_subdirs_api_method!(SUBDIRS))
-    .subdirs(SUBDIRS);
+    .subdirs(&SUBDIRS);
+
+pub const ROUTER: Router = Router::new()
+    .get(&API_METHOD_LIST_DRIVES)
+    .match_all("drive", &ITEM_ROUTER);