]> git.proxmox.com Git - proxmox-backup.git/blobdiff - src/api2/tape/drive.rs
update to proxmox-sys 0.2 crate
[proxmox-backup.git] / src / api2 / tape / drive.rs
index f4977612f1e2364349f95df217699f3621ea44d1..cedc2003e8c132f2dd024ec0f6102301c7efaaa5 100644 (file)
@@ -1,69 +1,63 @@
 use std::panic::UnwindSafe;
 use std::path::Path;
 use std::sync::Arc;
+use std::collections::HashMap;
 
 use anyhow::{bail, format_err, Error};
 use serde_json::Value;
 
-use proxmox::{
-    sortable,
-    identity,
-    list_subdirs_api_method,
-    tools::Uuid,
-    sys::error::SysError,
-    api::{
-        api,
-        section_config::SectionConfigData,
-        RpcEnvironment,
-        RpcEnvironmentType,
-        Router,
-        SubdirMap,
-    },
+use proxmox_sys::{sortable, identity};
+use proxmox_router::{
+    list_subdirs_api_method, Permission, Router, RpcEnvironment, RpcEnvironmentType, SubdirMap,
+};
+use proxmox_schema::api;
+use proxmox_section_config::SectionConfigData;
+use proxmox_uuid::Uuid;
+use proxmox_sys::{task_log, task_warn};
+
+use pbs_api_types::{
+    UPID_SCHEMA, CHANGER_NAME_SCHEMA, DRIVE_NAME_SCHEMA, MEDIA_LABEL_SCHEMA, MEDIA_POOL_NAME_SCHEMA,
+    Authid, DriveListEntry, LtoTapeDrive, MediaIdFlat, LabelUuidMap, MamAttribute,
+    LtoDriveAndMediaStatus, Lp17VolumeStatistics,
+};
+use pbs_api_types::{PRIV_TAPE_AUDIT, PRIV_TAPE_READ, PRIV_TAPE_WRITE};
+
+use pbs_config::CachedUserInfo;
+use pbs_tape::{
+    BlockReadError,
+    sg_tape::tape_alert_flags_critical,
+    linux_list_drives::{lto_tape_device_list, lookup_device_identification, open_lto_tape_device},
 };
+use proxmox_rest_server::WorkerTask;
 
 use crate::{
-    task_log,
-    config,
-    api2::{
-        types::{
-            UPID_SCHEMA,
-            CHANGER_NAME_SCHEMA,
-            DRIVE_NAME_SCHEMA,
-            MEDIA_LABEL_SCHEMA,
-            MEDIA_POOL_NAME_SCHEMA,
-            Authid,
-            DriveListEntry,
-            LinuxTapeDrive,
-            MediaIdFlat,
-            LabelUuidMap,
-            MamAttribute,
-            LinuxDriveAndMediaStatus,
-        },
-        tape::restore::restore_media,
+    api2::tape::restore::{
+        fast_catalog_restore,
+        restore_media,
     },
-    server::WorkerTask,
     tape::{
         TAPE_STATUS_DIR,
-        MediaPool,
         Inventory,
         MediaCatalog,
         MediaId,
-        linux_tape_device_list,
-        lookup_device_identification,
+        lock_media_set,
+        lock_media_pool,
+        lock_unassigned_media_pool,
         file_formats::{
             MediaLabel,
             MediaSetLabel,
         },
         drive::{
             TapeDriver,
-            LinuxTapeHandle,
-            Lp17VolumeStatistics,
-            open_linux_tape_device,
+            LtoTapeHandle,
+            open_lto_tape_drive,
             media_changer,
             required_media_changer,
             open_drive,
             lock_tape_device,
             set_tape_device_state,
+            get_tape_device_state,
         },
         changer::update_changer_online_status,
     },
@@ -83,10 +77,10 @@ where
         + FnOnce(Arc<WorkerTask>, SectionConfigData) -> Result<(), Error>,
 {
     // early check/lock before starting worker
-    let (config, _digest) = config::drive::config()?;
+    let (config, _digest) = pbs_config::drive::config()?;
     let lock_guard = lock_tape_device(&config, &drive)?;
 
-    let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
+    let auth_id = rpcenv.get_auth_id().unwrap();
     let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
 
     WorkerTask::new_thread(worker_type, job_id, auth_id, to_stdout, move |worker| {
@@ -101,6 +95,26 @@ where
     })
 }
 
+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) = pbs_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: {
@@ -115,6 +129,9 @@ where
     returns: {
         schema: UPID_SCHEMA,
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
+    },
 )]
 /// Load media with specified label
 ///
@@ -124,32 +141,19 @@ pub fn load_media(
     label_text: String,
     rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<Value, Error> {
-
-    let (config, _digest) = config::drive::config()?;
-
-    // early check/lock before starting worker
-    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;
-
     let job_id = format!("{}:{}", drive, label_text);
 
-    let upid_str = WorkerTask::new_thread(
+    let upid_str = run_drive_worker(
+        rpcenv,
+        drive.clone(),
         "load-media",
         Some(job_id),
-        auth_id,
-        to_stdout,
-        move |worker| {
-            let _lock_guard = lock_guard; // keep lock guard
-
+        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(())
-        }
+        },
     )?;
 
     Ok(upid_str.into())
@@ -167,21 +171,24 @@ pub fn load_media(
             },
         },
     },
+    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()?;
-    let lock_guard = lock_tape_device(&config, &drive)?;
-
-    tokio::task::spawn_blocking(move || {
-        let _lock_guard = lock_guard; // keep lock guard
-
-        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(
@@ -196,26 +203,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()?;
-    let lock_guard = lock_tape_device(&config, &drive)?;
-
-    tokio::task::spawn_blocking(move || {
-        let _lock_guard = lock_guard; // keep lock guard
-
-        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(
@@ -234,6 +247,9 @@ 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 fn unload(
@@ -241,29 +257,18 @@ pub fn unload(
     target_slot: Option<u64>,
     rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<Value, Error> {
-
-    let (config, _digest) = config::drive::config()?;
-    // early check/lock before starting worker
-    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;
-
-    let upid_str = WorkerTask::new_thread(
+    let upid_str = run_drive_worker(
+        rpcenv,
+        drive.clone(),
         "unload-media",
         Some(drive.clone()),
-        auth_id,
-        to_stdout,
-        move |worker| {
-            let _lock_guard = lock_guard; // keep lock guard
-
+        move |worker, config| {
             task_log!(worker, "unloading media from drive '{}'", drive);
 
             let (mut changer, _) = required_media_changer(&config, &drive)?;
             changer.unload_media(target_slot)?;
             Ok(())
-        }
+        },
     )?;
 
     Ok(upid_str.into())
@@ -290,31 +295,23 @@ pub fn unload(
     returns: {
         schema: UPID_SCHEMA,
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE, false),
+    },
 )]
-/// Erase media. Check for label-text if given (cancels if wrong media).
-pub fn erase_media(
+/// Format media. Check for label-text if given (cancels if wrong media).
+pub fn format_media(
     drive: String,
     fast: Option<bool>,
     label_text: Option<String>,
     rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<Value, Error> {
-    let (config, _digest) = config::drive::config()?;
-
-    // early check/lock before starting worker
-    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;
-
-    let upid_str = WorkerTask::new_thread(
-        "erase-media",
+    let upid_str = run_drive_worker(
+        rpcenv,
+        drive.clone(),
+        "format-media",
         Some(drive.clone()),
-        auth_id,
-        to_stdout,
-        move |worker| {
-            let _lock_guard = lock_guard; // keep lock guard
-
+        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)? {
@@ -331,15 +328,15 @@ pub fn erase_media(
                     }
                     /* 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))?;
+                    task_log!(worker, "format anyways");
+                    handle.format_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))?;
+                    task_log!(worker, "found empty media - format anyways");
+                    handle.format_media(fast.unwrap_or(true))?;
                 }
                 Ok((Some(media_id), _key_config)) => {
                     if let Some(label_text) = label_text {
@@ -359,16 +356,25 @@ pub fn erase_media(
                     );
 
                     let status_path = Path::new(TAPE_STATUS_DIR);
-                    let mut inventory = Inventory::load(status_path)?;
+                    let mut inventory = Inventory::new(status_path);
 
-                    MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
-                    inventory.remove_media(&media_id.label.uuid)?;
-                    handle.erase_media(fast.unwrap_or(true))?;
+                    if let Some(MediaSetLabel { ref pool, ref uuid, ..}) =  media_id.media_set_label {
+                        let _pool_lock = lock_media_pool(status_path, pool)?;
+                        let _media_set_lock = lock_media_set(status_path, uuid, None)?;
+                        MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
+                        inventory.remove_media(&media_id.label.uuid)?;
+                    } else {
+                        let _lock = lock_unassigned_media_pool(status_path)?;
+                        MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
+                        inventory.remove_media(&media_id.label.uuid)?;
+                    };
+
+                    handle.format_media(fast.unwrap_or(true))?;
                 }
             }
 
             Ok(())
-        }
+        },
     )?;
 
     Ok(upid_str.into())
@@ -385,33 +391,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()?;
-
-    // early check/lock before starting worker
-    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;
-
-    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| {
-            let _lock_guard = lock_guard; // keep lock guard
+        move |_worker, config| {
             let mut drive = open_drive(&config, &drive)?;
             drive.rewind()?;
             Ok(())
-        }
+        },
     )?;
 
     Ok(upid_str.into())
@@ -428,38 +426,30 @@ pub fn rewind(
     returns: {
         schema: UPID_SCHEMA,
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
+    },
 )]
 /// Eject/Unload drive media
 pub fn eject_media(
     drive: String,
     rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<Value, Error> {
-
-    let (config, _digest) = config::drive::config()?;
-
-    // early check/lock before starting worker
-    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;
-
-    let upid_str = WorkerTask::new_thread(
+    let upid_str = run_drive_worker(
+        rpcenv,
+        drive.clone(),
         "eject-media",
         Some(drive.clone()),
-        auth_id,
-        to_stdout,
-        move |_worker| {
-            let _lock_guard = lock_guard; // keep lock guard
-
-             if let Some((mut changer, _)) = media_changer(&config, &drive)? {
+        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(())
-        })?;
+        },
+    )?;
 
     Ok(upid_str.into())
 }
@@ -482,62 +472,49 @@ pub fn eject_media(
     returns: {
         schema: UPID_SCHEMA,
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE, false),
+    },
 )]
 /// Label media
 ///
 /// Write a new media label to the media in 'drive'. The media is
 /// assigned to the specified 'pool', or else to the free media pool.
 ///
-/// Note: The media need to be empty (you may want to erase it first).
+/// Note: The media need to be empty (you may want to format it first).
 pub fn label_media(
     drive: String,
     pool: Option<String>,
     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()?;
+        let (pool_config, _digest) = pbs_config::media_pool::config()?;
 
         if pool_config.sections.get(pool).is_none() {
             bail!("no such pool ('{}')", pool);
         }
     }
-
-    let (config, _digest) = config::drive::config()?;
-
-    // early check/lock before starting worker
-    let lock_guard = lock_tape_device(&config, &drive)?;
-
-    let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
-
-    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| {
-            let _lock_guard = lock_guard; // keep lock guard
-
+        move |worker, config| {
             let mut drive = open_drive(&config, &drive)?;
 
             drive.rewind()?;
 
             match drive.read_next_file() {
-                Ok(Some(_file)) => bail!("media is not empty (erase first)"),
-                Ok(None) => { /* EOF mark at BOT, assume tape is empty */ },
+                Ok(_reader) => bail!("media is not empty (format it first)"),
+                Err(BlockReadError::EndOfFile) => { /* EOF mark at BOT, assume tape is empty */ },
+                Err(BlockReadError::EndOfStream) => { /* tape is empty */ },
                 Err(err) => {
-                    if err.is_errno(nix::errno::Errno::ENOSPC) || err.is_errno(nix::errno::Errno::EIO) {
-                        /* assume tape is empty */
-                    } else {
-                        bail!("media read error - {}", err);
-                    }
+                    bail!("media read error - {}", err);
                 }
             }
 
-            let ctime = proxmox::tools::time::epoch_i64();
+            let ctime = proxmox_time::epoch_i64();
             let label = MediaLabel {
                 label_text: label_text.to_string(),
                 uuid: Uuid::generate(),
@@ -545,7 +522,7 @@ pub fn label_media(
             };
 
             write_media_label(worker, &mut drive, label, pool)
-        }
+        },
     )?;
 
     Ok(upid_str.into())
@@ -560,28 +537,37 @@ fn write_media_label(
 
     drive.label_tape(&label)?;
 
-    let mut media_set_label = None;
+    let status_path = Path::new(TAPE_STATUS_DIR);
 
-    if let Some(ref pool) = pool {
+    let media_id = if let Some(ref pool) = pool {
         // assign media to pool by writing special media set label
-        worker.log(format!("Label media '{}' for pool '{}'", label.label_text, pool));
+        task_log!(worker, "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, None)?;
-        media_set_label = Some(set);
+
+        let media_id = MediaId { label, media_set_label: Some(set) };
+
+        // Create the media catalog
+        MediaCatalog::overwrite(status_path, &media_id, false)?;
+
+        let mut inventory = Inventory::new(status_path);
+        inventory.store(media_id.clone(), false)?;
+
+        media_id
     } else {
-        worker.log(format!("Label media '{}' (no pool assignment)", label.label_text));
-    }
+        task_log!(worker, "Label media '{}' (no pool assignment)", label.label_text);
 
-    let media_id = MediaId { label, media_set_label };
+        let media_id = MediaId { label, media_set_label: None };
 
-    let status_path = Path::new(TAPE_STATUS_DIR);
+        // Create the media catalog
+        MediaCatalog::overwrite(status_path, &media_id, false)?;
 
-    // Create the media catalog
-    MediaCatalog::overwrite(status_path, &media_id, false)?;
+        let mut inventory = Inventory::new(status_path);
+        inventory.store(media_id.clone(), false)?;
 
-    let mut inventory = Inventory::load(status_path)?;
-    inventory.store(media_id.clone(), false)?;
+        media_id
+    };
 
     drive.rewind()?;
 
@@ -627,35 +613,35 @@ fn write_media_label(
             },
         },
     },
+    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 (config, _digest) = config::drive::config()?;
-
-    // early check/lock before starting worker
-    let lock_guard = lock_tape_device(&config, &drive)?;
-
-    tokio::task::spawn_blocking(move || {
-        let _lock_guard = lock_guard; // keep lock guard
-
-        let mut drive = open_drive(&config, &drive)?;
+            let (_media_id, key_config) = drive.read_label()?;
 
-        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)?;
+                pbs_config::tape_encryption_keys::insert_key(key, key_config, true)?;
+            } else {
+                bail!("media does not contain any encryption key configuration");
+            }
 
-        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(())
         }
-
-        Ok(())
-    }).await?
+    )
+    .await
 }
 
  #[api(
@@ -673,71 +659,81 @@ pub async fn restore_key(
     returns: {
         type: MediaIdFlat,
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
+    },
 )]
 /// 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()?;
-
-    // early check/lock before starting worker
-    let lock_guard = lock_tape_device(&config, &drive)?;
-
-    tokio::task::spawn_blocking(move || {
-        let _lock_guard = lock_guard; // keep lock guard
-
-        let mut drive = open_drive(&config, &drive)?;
-
-        let (media_id, _key_config) = drive.read_label()?;
-
-        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()));
+            let (media_id, _key_config) = drive.read_label()?;
+
+            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| fp.signature());
+
+                        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!("unable to load encryption key: {}", err);
+                        }
+                    }
 
-                    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::new(state_path);
+
+                        if let Some(MediaSetLabel { ref pool, ref uuid, ..}) =  media_id.media_set_label {
+                            let _pool_lock = lock_media_pool(state_path, pool)?;
+                            let _lock = lock_media_set(state_path, uuid, None)?;
+                            MediaCatalog::destroy_unrelated_catalog(state_path, &media_id)?;
+                            inventory.store(media_id, false)?;
+                        } else {
+                            let _lock = lock_unassigned_media_pool(state_path)?;
+                            MediaCatalog::destroy(state_path, &media_id.label.uuid)?;
+                            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(
@@ -751,40 +747,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()?;
-
-    // early check/lock before starting worker
-    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;
-
-    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| {
-            let _lock_guard = lock_guard; // keep lock guard
-
+        move |worker, config| {
             let (mut changer, _changer_name) = required_media_changer(&config, &drive)?;
 
-            worker.log("Starting drive clean");
+            task_log!(worker, "Starting drive clean");
 
             changer.clean_drive()?;
 
-            worker.log("Drive cleaned sucessfully");
+             if let Ok(drive_config) = config.lookup::<LtoTapeDrive>("lto", &drive) {
+                 // Note: clean_drive unloads the cleaning media, so we cannot use drive_config.open
+                 let mut handle = LtoTapeHandle::new(open_lto_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() {
+                         task_log!(worker, "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() {
+                     task_log!(worker, "Volume mounts: {}", volume_stats.volume_mounts);
+                     let wearout = volume_stats.volume_mounts * 2; // (*100.0/50.0);
+                     task_log!(worker, "Cleaning tape wearout: {}%", wearout);
+                 }
+             }
+
+            task_log!(worker, "Drive cleaned successfully");
 
             Ok(())
-        })?;
+        },
+    )?;
 
     Ok(upid_str.into())
 }
@@ -804,6 +814,9 @@ pub fn clean_drive(
             type: LabelUuidMap,
         },
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
+    },
 )]
 /// List known media labels (Changer Inventory)
 ///
@@ -815,49 +828,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()?;
-
-    // early check/lock before starting worker
-    let lock_guard = lock_tape_device(&config, &drive)?;
-
-    tokio::task::spawn_blocking(move || {
-        let _lock_guard = lock_guard; // keep lock guard
+            let label_text_list = changer.online_media_label_texts()?;
 
-        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.clone()) });
-            } else {
-                list.push(LabelUuidMap { label_text, uuid: None });
-            }
+            Ok(list)
         }
-
-        Ok(list)
-    }).await?
+    )
+    .await
 }
 
 #[api(
@@ -876,6 +886,9 @@ pub async fn inventory(
     returns: {
         schema: UPID_SCHEMA,
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
+    },
 )]
 /// Update inventory
 ///
@@ -891,29 +904,17 @@ pub fn update_inventory(
     read_all_labels: Option<bool>,
     rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<Value, Error> {
-
-    let (config, _digest) = config::drive::config()?;
-
-    // early check/lock before starting worker
-    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;
-
-    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| {
-            let _lock_guard = lock_guard; // keep lock guard
-
+        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("changer device does not list any media labels".to_string());
+                task_log!(worker, "changer device does not list any media labels");
             }
 
             let state_path = Path::new(TAPE_STATUS_DIR);
@@ -924,43 +925,53 @@ pub fn update_inventory(
 
             for label_text in label_text_list.iter() {
                 if label_text.starts_with("CLN") {
-                    worker.log(format!("skip cleaning unit '{}'", label_text));
+                    task_log!(worker, "skip cleaning unit '{}'", label_text);
                     continue;
                 }
 
                 let label_text = label_text.to_string();
 
                 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));
+                    task_log!(worker, "media '{}' already inventoried", label_text);
                     continue;
                 }
 
                 if let Err(err) = changer.load_media(&label_text) {
-                    worker.warn(format!("unable to load media '{}' - {}", label_text, err));
+                    task_warn!(worker, "unable to load media '{}' - {}", label_text, err);
                     continue;
                 }
 
                 let mut drive = open_drive(&config, &drive)?;
                 match drive.read_label() {
                     Err(err) => {
-                        worker.warn(format!("unable to read label form media '{}' - {}", label_text, err));
+                        task_warn!(worker, "unable to read label form media '{}' - {}", label_text, err);
                     }
                     Ok((None, _)) => {
-                        worker.log(format!("media '{}' is empty", label_text));
+                        task_log!(worker, "media '{}' is empty", label_text);
                     }
                     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));
+                            task_warn!(worker, "label text mismatch ({} != {})", label_text, media_id.label.label_text);
                             continue;
                         }
-                        worker.log(format!("inventorize media '{}' with uuid '{}'", label_text, media_id.label.uuid));
-                        inventory.store(media_id, false)?;
+                        task_log!(worker, "inventorize media '{}' with uuid '{}'", label_text, media_id.label.uuid);
+
+                        if let Some(MediaSetLabel { ref pool, ref uuid, ..}) =  media_id.media_set_label {
+                            let _pool_lock = lock_media_pool(state_path, pool)?;
+                            let _lock = lock_media_set(state_path, uuid, None)?;
+                            MediaCatalog::destroy_unrelated_catalog(state_path, &media_id)?;
+                            inventory.store(media_id, false)?;
+                        } else {
+                            let _lock = lock_unassigned_media_pool(state_path)?;
+                            MediaCatalog::destroy(state_path, &media_id.label.uuid)?;
+                            inventory.store(media_id, false)?;
+                        };
                     }
                 }
                 changer.unload_media(None)?;
             }
             Ok(())
-        }
+        },
     )?;
 
     Ok(upid_str.into())
@@ -982,6 +993,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(
@@ -989,33 +1003,20 @@ 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()?;
+        let (pool_config, _digest) = pbs_config::media_pool::config()?;
 
         if pool_config.sections.get(pool).is_none() {
             bail!("no such pool ('{}')", pool);
         }
     }
 
-    let (drive_config, _digest) = config::drive::config()?;
-
-    // early check/lock before starting worker
-    let lock_guard = lock_tape_device(&drive_config, &drive)?;
-
-    let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
-
-    let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
-
-    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| {
-            let _lock_guard = lock_guard; // keep lock guard
-            barcode_label_media_worker(worker, drive, &drive_config, pool)
-        }
+        move |worker, config| barcode_label_media_worker(worker, drive, &config, pool),
     )?;
 
     Ok(upid_str.into())
@@ -1027,10 +1028,12 @@ fn barcode_label_media_worker(
     drive_config: &SectionConfigData,
     pool: Option<String>,
 ) -> Result<(), Error> {
-
     let (mut changer, changer_name) = required_media_changer(drive_config, &drive)?;
 
-    let label_text_list = changer.online_media_label_texts()?;
+    let mut 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);
 
@@ -1047,14 +1050,14 @@ fn barcode_label_media_worker(
 
         inventory.reload()?;
         if inventory.find_media_by_label_text(&label_text).is_some() {
-            worker.log(format!("media '{}' already inventoried (already labeled)", label_text));
+            task_log!(worker, "media '{}' already inventoried (already labeled)", label_text);
             continue;
         }
 
-        worker.log(format!("checking/loading media '{}'", label_text));
+        task_log!(worker, "checking/loading media '{}'", label_text);
 
         if let Err(err) = changer.load_media(&label_text) {
-            worker.warn(format!("unable to load media '{}' - {}", label_text, err));
+            task_warn!(worker, "unable to load media '{}' - {}", label_text, err);
             continue;
         }
 
@@ -1062,22 +1065,19 @@ fn barcode_label_media_worker(
         drive.rewind()?;
 
         match drive.read_next_file() {
-            Ok(Some(_file)) => {
-                worker.log(format!("media '{}' is not empty (erase first)", label_text));
+            Ok(_reader) => {
+                task_log!(worker, "media '{}' is not empty (format it first)", label_text);
                 continue;
             }
-            Ok(None) => { /* EOF mark at BOT, assume tape is empty */ },
-            Err(err) => {
-                if err.is_errno(nix::errno::Errno::ENOSPC) || err.is_errno(nix::errno::Errno::EIO) {
-                    /* assume tape is empty */
-                } else {
-                    worker.warn(format!("media '{}' read error (maybe not empty - erase first)", label_text));
-                    continue;
-                }
+            Err(BlockReadError::EndOfFile) => { /* EOF mark at BOT, assume tape is empty */ },
+            Err(BlockReadError::EndOfStream) => { /* tape is empty */ },
+            Err(_err) => {
+                task_warn!(worker, "media '{}' read error (maybe not empty - format it first)", label_text);
+                continue;
             }
         }
 
-        let ctime = proxmox::tools::time::epoch_i64();
+        let ctime = proxmox_time::epoch_i64();
         let label = MediaLabel {
             label_text: label_text.to_string(),
             uuid: Uuid::generate(),
@@ -1105,18 +1105,23 @@ 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 _lock_guard = lock_tape_device(&config, &drive)?;
-
-    let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
-    let mut handle = drive_config.open()?;
-
-    handle.cartridge_memory()
+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: LtoTapeDrive = config.lookup("lto", &drive)?;
+            let mut handle = open_lto_tape_drive(&drive_config)?;
+
+            handle.cartridge_memory()
+        }
+    )
+    .await
 }
 
 #[api(
@@ -1130,18 +1135,23 @@ pub fn cartridge_memory(drive: String) -> Result<Vec<MamAttribute>, Error> {
     returns: {
         type: Lp17VolumeStatistics,
     },
+    access: {
+        permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT, false),
+    },
 )]
 /// Read Volume Statistics (SCSI log page 17h)
-pub fn volume_statistics(drive: String) -> Result<Lp17VolumeStatistics, Error> {
-
-    let (config, _digest) = config::drive::config()?;
-
-    let _lock_guard = lock_tape_device(&config, &drive)?;
-
-    let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
-    let mut handle = drive_config.open()?;
-
-    handle.volume_statistics()
+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: LtoTapeDrive = config.lookup("lto", &drive)?;
+            let mut handle = open_lto_tape_drive(&drive_config)?;
+
+            handle.volume_statistics()
+        }
+    )
+    .await
 }
 
 #[api(
@@ -1153,24 +1163,29 @@ pub fn volume_statistics(drive: String) -> Result<Lp17VolumeStatistics, Error> {
         },
     },
     returns: {
-        type: LinuxDriveAndMediaStatus,
+        type: LtoDriveAndMediaStatus,
+    },
+    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()?;
-
-    let _lock_guard = lock_tape_device(&config, &drive)?;
+pub async fn status(drive: String) -> Result<LtoDriveAndMediaStatus, Error> {
+    run_drive_blocking_task(
+        drive.clone(),
+        "reading drive status".to_string(),
+        move |config| {
+            let drive_config: LtoTapeDrive = config.lookup("lto", &drive)?;
 
-    let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
+            // Note: use open_lto_tape_device, because this also works if no medium loaded
+            let file = open_lto_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 = LtoTapeHandle::new(file)?;
 
-    let mut handle = LinuxTapeHandle::new(file);
-
-    handle.get_drive_and_media_status()
+            handle.get_drive_and_media_status()
+        }
+    )
+    .await
 }
 
 #[api(
@@ -1184,6 +1199,11 @@ pub fn status(drive: String) -> Result<LinuxDriveAndMediaStatus, Error> {
                 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,
@@ -1194,50 +1214,45 @@ 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(
     drive: String,
     force: Option<bool>,
+    scan: Option<bool>,
     verbose: Option<bool>,
     rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<Value, Error> {
-
     let verbose = verbose.unwrap_or(false);
     let force = force.unwrap_or(false);
+    let scan = scan.unwrap_or(false);
 
-    let (config, _digest) = config::drive::config()?;
-
-    // early check/lock before starting worker
-    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;
-
-    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| {
-            let _lock_guard = lock_guard; // keep lock guard
-
+        move |worker, config| {
             let mut drive = open_drive(&config, &drive)?;
 
             drive.rewind()?;
 
             let media_id = match drive.read_label()? {
                 (Some(media_id), key_config) => {
-                    worker.log(format!(
+                    task_log!(
+                        worker,
                         "found media label: {}",
                         serde_json::to_string_pretty(&serde_json::to_value(&media_id)?)?
-                    ));
+                    );
                     if key_config.is_some() {
-                        worker.log(format!(
+                        task_log!(
+                            worker,
                             "encryption key config: {}",
                             serde_json::to_string_pretty(&serde_json::to_value(&key_config)?)?
-                        ));
+                        );
                     }
                     media_id
                 },
@@ -1246,19 +1261,22 @@ pub fn catalog_media(
 
             let status_path = Path::new(TAPE_STATUS_DIR);
 
-            let mut inventory = Inventory::load(status_path)?;
-            inventory.store(media_id.clone(), false)?;
+            let mut inventory = Inventory::new(status_path);
 
-            let pool = match media_id.media_set_label {
+            let (_media_set_lock, media_set_uuid) = match media_id.media_set_label {
                 None => {
-                    worker.log("media is empty");
+                    task_log!(worker, "media is empty");
+                    let _lock = lock_unassigned_media_pool(status_path)?;
                     MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
+                    inventory.store(media_id.clone(), false)?;
                     return Ok(());
                 }
                 Some(ref set) => {
                     if set.uuid.as_ref() == [0u8;16] { // media is empty
-                        worker.log("media is empty");
+                        task_log!(worker, "media is empty");
+                        let _lock = lock_unassigned_media_pool(status_path)?;
                         MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
+                        inventory.store(media_id.clone(), false)?;
                         return Ok(());
                     }
                     let encrypt_fingerprint = set.encryption_key_fingerprint.clone()
@@ -1266,21 +1284,41 @@ pub fn catalog_media(
 
                     drive.set_encryption(encrypt_fingerprint)?;
 
-                    set.pool.clone()
+                    let _pool_lock = lock_media_pool(status_path, &set.pool)?;
+                    let media_set_lock = lock_media_set(status_path, &set.uuid, None)?;
+
+                    MediaCatalog::destroy_unrelated_catalog(status_path, &media_id)?;
+
+                    inventory.store(media_id.clone(), false)?;
+
+                    (media_set_lock, &set.uuid)
                 }
             };
 
-            let _lock = MediaPool::lock(status_path, &pool)?;
-
             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)?;
+            if !scan {
+                let media_set = inventory.compute_media_set_members(media_set_uuid)?;
 
-            Ok(())
+                if fast_catalog_restore(&worker, &mut drive, &media_set, &media_id.label.uuid)? {
+                    return Ok(())
+                }
 
-        }
+                task_log!(worker, "no catalog found");
+            }
+
+            task_log!(worker, "scanning entire media to reconstruct catalog");
+
+            drive.rewind()?;
+            drive.read_label()?; // skip over labels - we already read them above
+
+            let mut checked_chunks = HashMap::new();
+            restore_media(worker, &mut drive, &media_id, None, &mut checked_chunks, verbose)?;
+
+            Ok(())
+        },
     )?;
 
     Ok(upid_str.into())
@@ -1302,18 +1340,25 @@ pub fn catalog_media(
             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 (config, _) = pbs_config::drive::config()?;
 
-    let linux_drives = linux_tape_device_list();
+    let lto_drives = lto_tape_device_list();
 
-    let drive_list: Vec<LinuxTapeDrive> = config.convert_to_typed_array("linux")?;
+    let drive_list: Vec<LtoTapeDrive> = config.convert_to_typed_array("lto")?;
 
     let mut list = Vec::new();
 
@@ -1322,8 +1367,14 @@ pub fn list_drives(
             continue;
         }
 
-        let info = lookup_device_identification(&linux_drives, &drive.path);
-        let entry = DriveListEntry { config: drive, info };
+        let privs = user_info.lookup_privs(&auth_id, &["tape", "drive", &drive.name]);
+        if (privs & PRIV_TAPE_AUDIT) == 0 {
+            continue;
+        }
+
+        let info = lookup_device_identification(&lto_drives, &drive.path);
+        let state = get_tape_device_state(&config, &drive.name)?;
+        let entry = DriveListEntry { config: drive, info, state };
         list.push(entry);
     }
 
@@ -1353,9 +1404,9 @@ pub const SUBDIRS: SubdirMap = &sorted!([
             .post(&API_METHOD_EJECT_MEDIA)
     ),
     (
-        "erase-media",
+        "format-media",
         &Router::new()
-            .post(&API_METHOD_ERASE_MEDIA)
+            .post(&API_METHOD_FORMAT_MEDIA)
     ),
     (
         "export-media",
@@ -1381,7 +1432,7 @@ pub const SUBDIRS: SubdirMap = &sorted!([
     (
         "load-slot",
         &Router::new()
-            .put(&API_METHOD_LOAD_SLOT)
+            .post(&API_METHOD_LOAD_SLOT)
     ),
     (
         "cartridge-memory",
@@ -1398,6 +1449,11 @@ pub const SUBDIRS: SubdirMap = &sorted!([
         &Router::new()
             .get(&API_METHOD_READ_LABEL)
     ),
+    (
+        "restore-key",
+        &Router::new()
+            .post(&API_METHOD_RESTORE_KEY)
+    ),
     (
         "rewind",
         &Router::new()