From 46a1863f8893f9a25e9aa23559a8efef628bb111 Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Thu, 7 Jan 2021 14:26:43 +0100 Subject: [PATCH] tape: improve MediaChange trait We expose the whole MtxStatus, and we can load/store from/to specified slot numbers. --- src/api2/tape/backup.rs | 4 +- src/api2/tape/drive.rs | 90 +++++++++---------------- src/bin/proxmox-tape.rs | 84 ++++++++++++++++++++++- src/bin/proxmox_tape/drive.rs | 12 ---- src/tape/changer/linux_tape.rs | 64 +++++++++--------- src/tape/changer/mod.rs | 39 +++++++++-- src/tape/changer/mtx_wrapper.rs | 30 --------- src/tape/drive/virtual_tape.rs | 116 +++++++++++++++++++++++--------- src/tape/online_status_map.rs | 35 +++++++++- 9 files changed, 300 insertions(+), 174 deletions(-) diff --git a/src/api2/tape/backup.rs b/src/api2/tape/backup.rs index ffccd5f0..581f98de 100644 --- a/src/api2/tape/backup.rs +++ b/src/api2/tape/backup.rs @@ -145,11 +145,11 @@ fn update_media_online_status(drive: &str) -> Result { let mut has_changer = false; - if let Ok(Some((changer, changer_name))) = media_changer(&config, drive) { + if let Ok(Some((mut changer, changer_name))) = media_changer(&config, drive) { has_changer = true; - let changer_id_list = changer.list_media_changer_ids()?; + let changer_id_list = changer.online_media_changer_ids()?; let status_path = Path::new(TAPE_STATUS_DIR); let mut inventory = Inventory::load(status_path)?; diff --git a/src/api2/tape/drive.rs b/src/api2/tape/drive.rs index da2b701a..8a2b6cad 100644 --- a/src/api2/tape/drive.rs +++ b/src/api2/tape/drive.rs @@ -32,7 +32,6 @@ use crate::{ MEDIA_POOL_NAME_SCHEMA, Authid, LinuxTapeDrive, - ScsiTapeChanger, TapeDeviceInfo, MediaIdFlat, LabelUuidMap, @@ -50,8 +49,6 @@ use crate::{ Inventory, MediaCatalog, MediaId, - mtx_load, - mtx_unload, linux_tape_device_list, open_drive, media_changer, @@ -68,41 +65,6 @@ use crate::{ }, }; -#[api( - input: { - properties: { - drive: { - schema: DRIVE_NAME_SCHEMA, - }, - slot: { - description: "Source slot number", - minimum: 1, - }, - }, - }, -)] -/// Load media via changer from slot -pub async fn load_slot( - drive: String, - slot: u64, - _param: Value, -) -> Result<(), Error> { - - let (config, _digest) = config::drive::config()?; - - let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?; - - let changer: ScsiTapeChanger = match drive_config.changer { - Some(ref changer) => config.lookup("changer", changer)?, - None => bail!("drive '{}' has no associated changer", drive), - }; - - tokio::task::spawn_blocking(move || { - let drivenum = drive_config.changer_drive_id.unwrap_or(0); - mtx_load(&changer.path, slot, drivenum) - }).await? -} - #[api( input: { properties: { @@ -127,6 +89,31 @@ pub async fn load_media(drive: String, changer_id: String) -> Result<(), Error> changer.load_media(&changer_id) }).await? } +#[api( + input: { + properties: { + drive: { + schema: DRIVE_NAME_SCHEMA, + }, + "source-slot": { + description: "Source slot number.", + minimum: 1, + }, + }, + }, +)] +/// 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? +} #[api( input: { @@ -134,7 +121,7 @@ pub async fn load_media(drive: String, changer_id: String) -> Result<(), Error> drive: { schema: DRIVE_NAME_SCHEMA, }, - slot: { + "target-slot": { description: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.", minimum: 1, optional: true, @@ -145,7 +132,7 @@ pub async fn load_media(drive: String, changer_id: String) -> Result<(), Error> /// Unload media via changer pub async fn unload( drive: String, - slot: Option, + target_slot: Option, _param: Value, ) -> Result<(), Error> { @@ -153,19 +140,8 @@ pub async fn unload( let mut drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?; - let changer: ScsiTapeChanger = match drive_config.changer { - Some(ref changer) => config.lookup("changer", changer)?, - None => bail!("drive '{}' has no associated changer", drive), - }; - - let drivenum = drive_config.changer_drive_id.unwrap_or(0); - tokio::task::spawn_blocking(move || { - if let Some(slot) = slot { - mtx_unload(&changer.path, slot, drivenum) - } else { - drive_config.unload_media() - } + drive_config.unload_media(target_slot) }).await? } @@ -298,7 +274,7 @@ pub async fn eject_media(drive: String) -> Result<(), Error> { let mut drive = open_drive(&config, &drive)?; drive.eject_media()?; } - changer.unload_media()?; + changer.unload_media(None)?; } else { let mut drive = open_drive(&config, &drive)?; drive.eject_media()?; @@ -534,9 +510,9 @@ pub async fn inventory( let (config, _digest) = config::drive::config()?; tokio::task::spawn_blocking(move || { - let (changer, changer_name) = required_media_changer(&config, &drive)?; + let (mut changer, changer_name) = required_media_changer(&config, &drive)?; - let changer_id_list = changer.list_media_changer_ids()?; + let changer_id_list = changer.online_media_changer_ids()?; let state_path = Path::new(TAPE_STATUS_DIR); @@ -619,7 +595,7 @@ pub fn update_inventory( let (mut changer, changer_name) = required_media_changer(&config, &drive)?; - let changer_id_list = changer.list_media_changer_ids()?; + let changer_id_list = changer.online_media_changer_ids()?; if changer_id_list.is_empty() { worker.log(format!("changer device does not list any media labels")); } @@ -734,7 +710,7 @@ fn barcode_label_media_worker( let (mut changer, changer_name) = required_media_changer(&config, &drive)?; - let changer_id_list = changer.list_media_changer_ids()?; + let changer_id_list = changer.online_media_changer_ids()?; let state_path = Path::new(TAPE_STATUS_DIR); diff --git a/src/bin/proxmox-tape.rs b/src/bin/proxmox-tape.rs index 33d10c47..610c64d6 100644 --- a/src/bin/proxmox-tape.rs +++ b/src/bin/proxmox-tape.rs @@ -216,7 +216,7 @@ async fn eject_media( }, }, )] -/// Load media +/// Load media with specified label async fn load_media( mut param: Value, rpcenv: &mut dyn RpcEnvironment, @@ -236,6 +236,77 @@ async fn load_media( Ok(()) } +#[api( + input: { + properties: { + drive: { + schema: DRIVE_NAME_SCHEMA, + optional: true, + }, + "source-slot": { + description: "Source slot number.", + type: u64, + minimum: 1, + }, + }, + }, +)] +/// Load media from the specified slot +async fn load_media_from_slot( + mut param: Value, + rpcenv: &mut dyn RpcEnvironment, +) -> Result<(), Error> { + + let (config, _digest) = config::drive::config()?; + + param["drive"] = lookup_drive_name(¶m, &config)?.into(); + + let info = &api2::tape::drive::API_METHOD_LOAD_SLOT; + + match info.handler { + ApiHandler::Async(handler) => (handler)(param, info, rpcenv).await?, + _ => unreachable!(), + }; + + Ok(()) +} + +#[api( + input: { + properties: { + drive: { + schema: DRIVE_NAME_SCHEMA, + optional: true, + }, + "target-slot": { + description: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.", + type: u64, + minimum: 1, + optional: true, + }, + }, + }, +)] +/// Unload media via changer +async fn unload_media( + mut param: Value, + rpcenv: &mut dyn RpcEnvironment, +) -> Result<(), Error> { + + let (config, _digest) = config::drive::config()?; + + param["drive"] = lookup_drive_name(¶m, &config)?.into(); + + let info = &api2::tape::drive::API_METHOD_UNLOAD; + + match info.handler { + ApiHandler::Async(handler) => (handler)(param, info, rpcenv).await?, + _ => unreachable!(), + }; + + Ok(()) +} + #[api( input: { properties: { @@ -803,6 +874,17 @@ fn main() { .completion_cb("drive", complete_drive_name) .completion_cb("changer-id", complete_media_changer_id) ) + .insert( + "load-media-from-slot", + CliCommand::new(&API_METHOD_LOAD_MEDIA_FROM_SLOT) + .arg_param(&["slot"]) + .completion_cb("drive", complete_drive_name) + ) + .insert( + "unload", + CliCommand::new(&API_METHOD_UNLOAD_MEDIA) + .completion_cb("drive", complete_drive_name) + ) ; let mut rpcenv = CliEnvironment::new(); diff --git a/src/bin/proxmox_tape/drive.rs b/src/bin/proxmox_tape/drive.rs index 73e34afc..217b7c86 100644 --- a/src/bin/proxmox_tape/drive.rs +++ b/src/bin/proxmox_tape/drive.rs @@ -59,18 +59,6 @@ pub fn drive_commands() -> CommandLineInterface { .completion_cb("path", complete_drive_path) .completion_cb("changer", complete_changer_name) ) - .insert( - "load", - CliCommand::new(&api2::tape::drive::API_METHOD_LOAD_SLOT) - .arg_param(&["drive"]) - .completion_cb("drive", complete_linux_drive_name) - ) - .insert( - "unload", - CliCommand::new(&api2::tape::drive::API_METHOD_UNLOAD) - .arg_param(&["drive"]) - .completion_cb("drive", complete_linux_drive_name) - ) ; cmd_def.into() diff --git a/src/tape/changer/linux_tape.rs b/src/tape/changer/linux_tape.rs index 51c2c963..a8b6d349 100644 --- a/src/tape/changer/linux_tape.rs +++ b/src/tape/changer/linux_tape.rs @@ -43,6 +43,30 @@ fn unload_to_free_slot(drive_name: &str, path: &str, status: &MtxStatus, drivenu impl MediaChange for LinuxTapeDrive { + fn status(&mut self) -> Result { + let (config, _digest) = crate::config::drive::config()?; + + let changer: ScsiTapeChanger = match self.changer { + Some(ref changer) => config.lookup("changer", changer)?, + None => bail!("drive '{}' has no associated changer", self.name), + }; + + mtx_status(&changer) + } + + fn load_media_from_slot(&mut self, slot: u64) -> Result<(), Error> { + let (config, _digest) = crate::config::drive::config()?; + + let changer: ScsiTapeChanger = match self.changer { + Some(ref changer) => config.lookup("changer", changer)?, + None => bail!("drive '{}' has no associated changer", self.name), + }; + + let drivenum = self.changer_drive_id.unwrap_or(0); + + mtx_load(&changer.path, slot, drivenum as u64) + } + fn load_media(&mut self, changer_id: &str) -> Result<(), Error> { if changer_id.starts_with("CLN") { @@ -97,11 +121,10 @@ impl MediaChange for LinuxTapeDrive { Some(slot) => slot, }; - mtx_load(&changer.path, slot as u64, drivenum as u64) } - fn unload_media(&mut self) -> Result<(), Error> { + fn unload_media(&mut self, target_slot: Option) -> Result<(), Error> { let (config, _digest) = crate::config::drive::config()?; let changer: ScsiTapeChanger = match self.changer { @@ -111,40 +134,15 @@ impl MediaChange for LinuxTapeDrive { let drivenum = self.changer_drive_id.unwrap_or(0); - let status = mtx_status(&changer)?; - - unload_to_free_slot(&self.name, &changer.path, &status, drivenum) + if let Some(target_slot) = target_slot { + mtx_unload(&changer.path, target_slot, drivenum) + } else { + let status = mtx_status(&changer)?; + unload_to_free_slot(&self.name, &changer.path, &status, drivenum) + } } fn eject_on_unload(&self) -> bool { true } - - fn list_media_changer_ids(&self) -> Result, Error> { - let (config, _digest) = crate::config::drive::config()?; - - let changer: ScsiTapeChanger = match self.changer { - Some(ref changer) => config.lookup("changer", changer)?, - None => return Ok(Vec::new()), - }; - - let status = mtx_status(&changer)?; - - let mut list = Vec::new(); - - for drive_status in status.drives.iter() { - if let ElementStatus::VolumeTag(ref tag) = drive_status.status { - list.push(tag.clone()); - } - } - - for (import_export, element_status) in status.slots.iter() { - if *import_export { continue; } - if let ElementStatus::VolumeTag(ref tag) = element_status { - list.push(tag.clone()); - } - } - - Ok(list) - } } diff --git a/src/tape/changer/mod.rs b/src/tape/changer/mod.rs index aba728a3..947b187a 100644 --- a/src/tape/changer/mod.rs +++ b/src/tape/changer/mod.rs @@ -15,21 +15,52 @@ use anyhow::Error; /// Interface to media change devices pub trait MediaChange { - /// Load media into drive + /// Returns the changer status + fn status(&mut self) -> Result; + + /// Load media from storage slot into drive + fn load_media_from_slot(&mut self, slot: u64) -> Result<(), Error>; + + /// Load media by changer-id into drive /// /// This unloads first if the drive is already loaded with another media. + /// + /// Note: This refuses to load media inside import/export slots. fn load_media(&mut self, changer_id: &str) -> Result<(), Error>; /// Unload media from drive /// /// This is a nop on drives without autoloader. - fn unload_media(&mut self) -> Result<(), Error>; + fn unload_media(&mut self, target_slot: Option) -> Result<(), Error>; /// Returns true if unload_media automatically ejects drive media fn eject_on_unload(&self) -> bool { false } - /// List media changer IDs (barcodes) - fn list_media_changer_ids(&self) -> Result, Error>; + /// List online media changer IDs (barcodes) + /// + /// List acessible (online) changer IDs. This does not include + /// media inside import-export slots or cleaning media. + fn online_media_changer_ids(&mut self) -> Result, Error> { + let status = self.status()?; + + let mut list = Vec::new(); + + for drive_status in status.drives.iter() { + if let ElementStatus::VolumeTag(ref tag) = drive_status.status { + list.push(tag.clone()); + } + } + + for (import_export, element_status) in status.slots.iter() { + if *import_export { continue; } + if let ElementStatus::VolumeTag(ref tag) = element_status { + if !tag.starts_with("CLN") { continue; } + list.push(tag.clone()); + } + } + + Ok(list) + } } diff --git a/src/tape/changer/mtx_wrapper.rs b/src/tape/changer/mtx_wrapper.rs index d8298653..29062422 100644 --- a/src/tape/changer/mtx_wrapper.rs +++ b/src/tape/changer/mtx_wrapper.rs @@ -4,7 +4,6 @@ use anyhow::Error; use serde_json::Value; use proxmox::{ - tools::Uuid, api::schema::parse_property_string, }; @@ -15,10 +14,8 @@ use crate::{ ScsiTapeChanger, }, tape::{ - Inventory, changer::{ MtxStatus, - ElementStatus, parse_mtx_status, }, }, @@ -100,30 +97,3 @@ pub fn mtx_transfer( Ok(()) } - -/// Extract the list of online media from MtxStatus -/// -/// Returns a HashSet containing all found media Uuid -pub fn mtx_status_to_online_set(status: &MtxStatus, inventory: &Inventory) -> HashSet { - - let mut online_set = HashSet::new(); - - for drive_status in status.drives.iter() { - if let ElementStatus::VolumeTag(ref changer_id) = drive_status.status { - if let Some(media_id) = inventory.find_media_by_changer_id(changer_id) { - online_set.insert(media_id.label.uuid.clone()); - } - } - } - - for (import_export, slot_status) in status.slots.iter() { - if *import_export { continue; } - if let ElementStatus::VolumeTag(ref changer_id) = slot_status { - if let Some(media_id) = inventory.find_media_by_changer_id(changer_id) { - online_set.insert(media_id.label.uuid.clone()); - } - } - } - - online_set -} diff --git a/src/tape/drive/virtual_tape.rs b/src/tape/drive/virtual_tape.rs index b7a8f5e7..617dbce4 100644 --- a/src/tape/drive/virtual_tape.rs +++ b/src/tape/drive/virtual_tape.rs @@ -14,6 +14,9 @@ use crate::{ tape::{ TapeWrite, TapeRead, + MtxStatus, + DriveStatus, + ElementStatus, changer::MediaChange, drive::{ VirtualTapeDrive, @@ -73,15 +76,7 @@ pub struct VirtualTapeHandle { _lock: File, } -impl VirtualTapeHandle { - - pub fn insert_tape(&self, _tape_filename: &str) { - unimplemented!(); - } - - pub fn eject_tape(&self) { - unimplemented!(); - } +impl VirtualTapeHandle { fn status_file_path(&self) -> std::path::PathBuf { let mut path = self.path.clone(); @@ -159,6 +154,25 @@ impl VirtualTapeHandle { replace_file(&path, raw.as_bytes(), options)?; Ok(()) } + + fn online_media_changer_ids(&self) -> Result, Error> { + let mut list = Vec::new(); + for entry in std::fs::read_dir(&self.path)? { + let entry = entry?; + let path = entry.path(); + if path.is_file() && path.extension() == Some(std::ffi::OsStr::new("json")) { + if let Some(name) = path.file_stem() { + if let Some(name) = name.to_str() { + if name.starts_with("tape-") { + list.push(name[5..].to_string()); + } + } + } + } + } + Ok(list) + } + } impl TapeDriver for VirtualTapeHandle { @@ -348,6 +362,51 @@ impl TapeDriver for VirtualTapeHandle { impl MediaChange for VirtualTapeHandle { + fn status(&mut self) -> Result { + + let drive_status = self.load_status()?; + + let mut drives = Vec::new(); + + if let Some(current_tape) = &drive_status.current_tape { + drives.push(DriveStatus { + loaded_slot: None, + status: ElementStatus::VolumeTag(current_tape.name.clone()), + }); + } + + // This implementation is lame, because we do not have fixed + // slot-assignment here. + + let mut slots = Vec::new(); + let changer_ids = self.online_media_changer_ids()?; + let max_slots = ((changer_ids.len() + 7)/8) * 8; + + for i in 0..max_slots { + if let Some(changer_id) = changer_ids.get(i) { + slots.push((false, ElementStatus::VolumeTag(changer_id.clone()))); + } else { + slots.push((false, ElementStatus::Empty)); + } + } + + Ok(MtxStatus { drives, slots }) + } + + fn load_media_from_slot(&mut self, slot: u64) -> Result<(), Error> { + if slot < 1 { + bail!("invalid slot ID {}", slot); + } + + let changer_ids = self.online_media_changer_ids()?; + + if slot > changer_ids.len() as u64 { + bail!("slot {} is empty", slot); + } + + self.load_media(&changer_ids[slot as usize - 1]) + } + /// Try to load media /// /// We automatically create an empty virtual tape here (if it does @@ -371,7 +430,8 @@ impl MediaChange for VirtualTapeHandle { self.store_status(&status) } - fn unload_media(&mut self) -> Result<(), Error> { + fn unload_media(&mut self, _target_slot: Option) -> Result<(), Error> { + // Note: we currently simply ignore target_slot self.eject_media()?; Ok(()) } @@ -379,36 +439,28 @@ impl MediaChange for VirtualTapeHandle { fn eject_on_unload(&self) -> bool { true } - - fn list_media_changer_ids(&self) -> Result, Error> { - let mut list = Vec::new(); - for entry in std::fs::read_dir(&self.path)? { - let entry = entry?; - let path = entry.path(); - if path.is_file() && path.extension() == Some(std::ffi::OsStr::new("json")) { - if let Some(name) = path.file_stem() { - if let Some(name) = name.to_str() { - if name.starts_with("tape-") { - list.push(name[5..].to_string()); - } - } - } - } - } - Ok(list) - } } impl MediaChange for VirtualTapeDrive { + fn status(&mut self) -> Result { + let mut handle = self.open()?; + handle.status() + } + + fn load_media_from_slot(&mut self, slot: u64) -> Result<(), Error> { + let mut handle = self.open()?; + handle.load_media_from_slot(slot) + } + fn load_media(&mut self, changer_id: &str) -> Result<(), Error> { let mut handle = self.open()?; handle.load_media(changer_id) } - fn unload_media(&mut self) -> Result<(), Error> { + fn unload_media(&mut self, target_slot: Option) -> Result<(), Error> { let mut handle = self.open()?; - handle.eject_media()?; + handle.unload_media(target_slot)?; Ok(()) } @@ -416,8 +468,8 @@ impl MediaChange for VirtualTapeDrive { true } - fn list_media_changer_ids(&self) -> Result, Error> { + fn online_media_changer_ids(&mut self) -> Result, Error> { let handle = self.open()?; - handle.list_media_changer_ids() + handle.online_media_changer_ids() } } diff --git a/src/tape/online_status_map.rs b/src/tape/online_status_map.rs index 1b6d3f96..79b1d2bd 100644 --- a/src/tape/online_status_map.rs +++ b/src/tape/online_status_map.rs @@ -14,8 +14,9 @@ use crate::{ tape::{ MediaChange, Inventory, + MtxStatus, + ElementStatus, mtx_status, - mtx_status_to_online_set, }, }; @@ -89,6 +90,34 @@ impl OnlineStatusMap { } } +/// Extract the list of online media from MtxStatus +/// +/// Returns a HashSet containing all found media Uuid. This only +/// returns media found in Inventory. +pub fn mtx_status_to_online_set(status: &MtxStatus, inventory: &Inventory) -> HashSet { + + let mut online_set = HashSet::new(); + + for drive_status in status.drives.iter() { + if let ElementStatus::VolumeTag(ref changer_id) = drive_status.status { + if let Some(media_id) = inventory.find_media_by_changer_id(changer_id) { + online_set.insert(media_id.label.uuid.clone()); + } + } + } + + for (import_export, slot_status) in status.slots.iter() { + if *import_export { continue; } + if let ElementStatus::VolumeTag(ref changer_id) = slot_status { + if let Some(media_id) = inventory.find_media_by_changer_id(changer_id) { + online_set.insert(media_id.label.uuid.clone()); + } + } + } + + online_set +} + /// Update online media status /// /// Simply ask all changer devices. @@ -116,8 +145,8 @@ pub fn update_online_status(state_path: &Path) -> Result } let vtapes: Vec = config.convert_to_typed_array("virtual")?; - for vtape in vtapes { - let media_list = match vtape.list_media_changer_ids() { + for mut vtape in vtapes { + let media_list = match vtape.online_media_changer_ids() { Ok(media_list) => media_list, Err(err) => { eprintln!("unable to get changer '{}' status - {}", vtape.name, err);