]> git.proxmox.com Git - proxmox-backup.git/blobdiff - src/tape/changer/mod.rs
fix #4904: tape changer: add option to eject before unload
[proxmox-backup.git] / src / tape / changer / mod.rs
index cc9ee7342ee01f0c65838e5d31b87078ab5bb652..df63f6f8d2926e3f01126bd7dab0b2db46b893b3 100644 (file)
@@ -4,20 +4,22 @@ pub mod mtx;
 
 mod online_status_map;
 pub use online_status_map::*;
+use proxmox_schema::ApiType;
 
 use std::path::PathBuf;
 
 use anyhow::{bail, Error};
 
-use proxmox::tools::fs::{CreateOptions, replace_file, file_read_optional_string};
+use proxmox_sys::fs::{file_read_optional_string, replace_file, CreateOptions};
 
-use pbs_api_types::{ScsiTapeChanger, LtoTapeDrive};
+use pbs_api_types::{ChangerOptions, LtoTapeDrive, ScsiTapeChanger};
 
-use pbs_tape::{sg_pt_changer, MtxStatus, ElementStatus};
+use pbs_tape::{linux_list_drives::open_lto_tape_device, sg_pt_changer, ElementStatus, MtxStatus};
+
+use crate::tape::drive::{LtoTapeHandle, TapeDriver};
 
 /// Interface to SCSI changer devices
 pub trait ScsiMediaChange {
-
     fn status(&mut self, use_cache: bool) -> Result<MtxStatus, Error>;
 
     fn load_slot(&mut self, from_slot: u64, drivenum: u64) -> Result<MtxStatus, Error>;
@@ -29,7 +31,6 @@ pub trait ScsiMediaChange {
 
 /// Interface to the media changer device for a single drive
 pub trait MediaChange {
-
     /// Drive number inside changer
     fn drive_number(&self) -> u64;
 
@@ -55,9 +56,11 @@ pub trait MediaChange {
     /// slots. Also, you cannot load cleaning units with this
     /// interface.
     fn load_media(&mut self, label_text: &str) -> Result<MtxStatus, Error> {
-
         if label_text.starts_with("CLN") {
-            bail!("unable to load media '{}' (seems to be a cleaning unit)", label_text);
+            bail!(
+                "unable to load media '{}' (seems to be a cleaning unit)",
+                label_text
+            );
         }
 
         let mut status = self.status()?;
@@ -69,17 +72,21 @@ pub trait MediaChange {
             if let ElementStatus::VolumeTag(ref tag) = drive_status.status {
                 if *tag == label_text {
                     if i as u64 != self.drive_number() {
-                        bail!("unable to load media '{}' - media in wrong drive ({} != {})",
-                              label_text, i, self.drive_number());
+                        bail!(
+                            "unable to load media '{}' - media in wrong drive ({} != {})",
+                            label_text,
+                            i,
+                            self.drive_number()
+                        );
                     }
-                    return Ok(status) // already loaded
+                    return Ok(status); // already loaded
                 }
             }
             if i as u64 == self.drive_number() {
                 match drive_status.status {
-                    ElementStatus::Empty => { /* OK */ },
+                    ElementStatus::Empty => { /* OK */ }
                     _ => unload_drive = true,
-                 }
+                }
             }
         }
 
@@ -92,9 +99,12 @@ pub trait MediaChange {
             if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
                 if tag == label_text {
                     if slot_info.import_export {
-                        bail!("unable to load media '{}' - inside import/export slot", label_text);
+                        bail!(
+                            "unable to load media '{}' - inside import/export slot",
+                            label_text
+                        );
                     }
-                    slot = Some(i+1);
+                    slot = Some(i + 1);
                     break;
                 }
             }
@@ -127,9 +137,13 @@ pub trait MediaChange {
         }
 
         for slot_info in status.slots.iter() {
-            if slot_info.import_export { continue; }
+            if slot_info.import_export {
+                continue;
+            }
             if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
-                if tag.starts_with("CLN") { continue; }
+                if tag.starts_with("CLN") {
+                    continue;
+                }
                 list.push(tag.clone());
             }
         }
@@ -147,15 +161,19 @@ pub trait MediaChange {
         // Unload drive first. Note: This also unloads a loaded cleaning tape
         if let Some(drive_status) = status.drives.get(self.drive_number() as usize) {
             match drive_status.status {
-                ElementStatus::Empty => { /* OK */ },
-                _ => { status = self.unload_to_free_slot(status)?; }
+                ElementStatus::Empty => { /* OK */ }
+                _ => {
+                    status = self.unload_to_free_slot(status)?;
+                }
             }
         }
 
         let mut cleaning_cartridge_slot = None;
 
         for (i, slot_info) in status.slots.iter().enumerate() {
-            if slot_info.import_export { continue; }
+            if slot_info.import_export {
+                continue;
+            }
             if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
                 if tag.starts_with("CLN") {
                     cleaning_cartridge_slot = Some(i + 1);
@@ -169,7 +187,6 @@ pub trait MediaChange {
             Some(cleaning_cartridge_slot) => cleaning_cartridge_slot as u64,
         };
 
-
         self.load_media_from_slot(cleaning_cartridge_slot)?;
 
         self.unload_media(Some(cleaning_cartridge_slot))
@@ -197,7 +214,9 @@ pub trait MediaChange {
 
         for (i, slot_info) in status.slots.iter().enumerate() {
             if slot_info.import_export {
-                if to.is_some() { continue; }
+                if to.is_some() {
+                    continue;
+                }
                 if let ElementStatus::Empty = slot_info.status {
                     to = Some(i as u64 + 1);
                 }
@@ -214,7 +233,7 @@ pub trait MediaChange {
                     self.unload_media(Some(to))?;
                     Ok(Some(to))
                 }
-                None =>  bail!("unable to find free export slot"),
+                None => bail!("unable to find free export slot"),
             }
         } else {
             match (from, to) {
@@ -234,7 +253,6 @@ pub trait MediaChange {
     ///
     /// Note: This method consumes status - so please use returned status afterward.
     fn unload_to_free_slot(&mut self, status: MtxStatus) -> Result<MtxStatus, Error> {
-
         let drive_status = &status.drives[self.drive_number() as usize];
         if let Some(slot) = drive_status.loaded_slot {
             // check if original slot is empty/usable
@@ -248,7 +266,10 @@ pub trait MediaChange {
         if let Some(slot) = status.find_free_slot(false) {
             self.unload_media(Some(slot))
         } else {
-            bail!("drive '{}' unload failure - no free slot", self.drive_name());
+            bail!(
+                "drive '{}' unload failure - no free slot",
+                self.drive_name()
+            );
         }
     }
 }
@@ -256,8 +277,7 @@ pub trait MediaChange {
 const USE_MTX: bool = false;
 
 impl ScsiMediaChange for ScsiTapeChanger {
-
-    fn status(&mut self, use_cache: bool)  -> Result<MtxStatus, Error> {
+    fn status(&mut self, use_cache: bool) -> Result<MtxStatus, Error> {
         if use_cache {
             if let Some(state) = load_changer_state_cache(&self.name)? {
                 return Ok(state);
@@ -265,9 +285,9 @@ impl ScsiMediaChange for ScsiTapeChanger {
         }
 
         let status = if USE_MTX {
-            mtx::mtx_status(&self)
+            mtx::mtx_status(self)
         } else {
-            sg_pt_changer::status(&self)
+            sg_pt_changer::status(self)
         };
 
         match &status {
@@ -328,11 +348,7 @@ impl ScsiMediaChange for ScsiTapeChanger {
     }
 }
 
-fn save_changer_state_cache(
-    changer: &str,
-    state: &MtxStatus,
-) -> Result<(), Error> {
-
+fn save_changer_state_cache(changer: &str, state: &MtxStatus) -> Result<(), Error> {
     let mut path = PathBuf::from(crate::tape::CHANGER_STATE_DIR);
     path.push(changer);
 
@@ -345,7 +361,7 @@ fn save_changer_state_cache(
         .owner(backup_user.uid)
         .group(backup_user.gid);
 
-    replace_file(path, state.as_bytes(), options)
+    replace_file(path, state.as_bytes(), options, false)
 }
 
 fn delete_changer_state_cache(changer: &str) {
@@ -371,13 +387,11 @@ fn load_changer_state_cache(changer: &str) -> Result<Option<MtxStatus>, Error> {
 
 /// Implements MediaChange using 'mtx' linux cli tool
 pub struct MtxMediaChanger {
-    drive_name: String, // used for error messages
-    drive_number: u64,
+    drive: LtoTapeDrive,
     config: ScsiTapeChanger,
 }
 
 impl MtxMediaChanger {
-
     pub fn with_drive_config(drive_config: &LtoTapeDrive) -> Result<Self, Error> {
         let (config, _digest) = pbs_config::drive::config()?;
         let changer_config: ScsiTapeChanger = match drive_config.changer {
@@ -386,21 +400,19 @@ impl MtxMediaChanger {
         };
 
         Ok(Self {
-            drive_name: drive_config.name.clone(),
-            drive_number: drive_config.changer_drivenum.unwrap_or(0),
+            drive: drive_config.clone(),
             config: changer_config,
         })
     }
 }
 
 impl MediaChange for MtxMediaChanger {
-
     fn drive_number(&self) -> u64 {
-        self.drive_number
+        self.drive.changer_drivenum.unwrap_or(0)
     }
 
     fn drive_name(&self) -> &str {
-        &self.drive_name
+        &self.drive.name
     }
 
     fn status(&mut self) -> Result<MtxStatus, Error> {
@@ -412,12 +424,26 @@ impl MediaChange for MtxMediaChanger {
     }
 
     fn load_media_from_slot(&mut self, slot: u64) -> Result<MtxStatus, Error> {
-        self.config.load_slot(slot, self.drive_number)
+        self.config.load_slot(slot, self.drive_number())
     }
 
     fn unload_media(&mut self, target_slot: Option<u64>) -> Result<MtxStatus, Error> {
+        let options: ChangerOptions = serde_json::from_value(
+            ChangerOptions::API_SCHEMA
+                .parse_property_string(self.config.options.as_deref().unwrap_or_default())?,
+        )?;
+
+        if options.eject_before_unload {
+            let file = open_lto_tape_device(&self.drive.path)?;
+            let mut handle = LtoTapeHandle::new(file)?;
+
+            if handle.medium_present() {
+                handle.eject_media()?;
+            }
+        }
+
         if let Some(target_slot) = target_slot {
-            self.config.unload(target_slot, self.drive_number)
+            self.config.unload(target_slot, self.drive_number())
         } else {
             let status = self.status()?;
             self.unload_to_free_slot(status)