From b107fdb99a9b85299f386df7e9dbc31d7aa92484 Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Sat, 5 Dec 2020 14:46:57 +0100 Subject: [PATCH] tape: add tape changer support using 'mtx' command --- src/tape/changer/email.rs | 57 ++++++++++ src/tape/changer/linux_tape.rs | 145 ++++++++++++++++++++++++ src/tape/changer/mod.rs | 35 ++++++ src/tape/changer/mtx_wrapper.rs | 84 ++++++++++++++ src/tape/changer/parse_mtx_status.rs | 161 +++++++++++++++++++++++++++ src/tape/mod.rs | 3 + 6 files changed, 485 insertions(+) create mode 100644 src/tape/changer/email.rs create mode 100644 src/tape/changer/linux_tape.rs create mode 100644 src/tape/changer/mod.rs create mode 100644 src/tape/changer/mtx_wrapper.rs create mode 100644 src/tape/changer/parse_mtx_status.rs diff --git a/src/tape/changer/email.rs b/src/tape/changer/email.rs new file mode 100644 index 00000000..ec67718c --- /dev/null +++ b/src/tape/changer/email.rs @@ -0,0 +1,57 @@ +use anyhow::Error; + +use proxmox::tools::email::sendmail; + +use super::MediaChange; + +/// Send email to a person to request a manual media change +pub struct ChangeMediaEmail { + drive: String, + to: String, +} + +impl ChangeMediaEmail { + + pub fn new(drive: &str, to: &str) -> Self { + Self { + drive: String::from(drive), + to: String::from(to), + } + } +} + +impl MediaChange for ChangeMediaEmail { + + fn load_media(&mut self, changer_id: &str) -> Result<(), Error> { + + let subject = format!("Load Media '{}' request for drive '{}'", changer_id, self.drive); + + let mut text = String::new(); + + text.push_str("Please insert the requested media into the backup drive.\n\n"); + + text.push_str(&format!("Drive: {}\n", self.drive)); + text.push_str(&format!("Media: {}\n", changer_id)); + + sendmail( + &[&self.to], + &subject, + Some(&text), + None, + None, + None, + )?; + + Ok(()) + } + + fn unload_media(&mut self) -> Result<(), Error> { + /* ignore ? */ + Ok(()) + } + + fn list_media_changer_ids(&self) -> Result, Error> { + Ok(Vec::new()) + } + +} diff --git a/src/tape/changer/linux_tape.rs b/src/tape/changer/linux_tape.rs new file mode 100644 index 00000000..e2a4a404 --- /dev/null +++ b/src/tape/changer/linux_tape.rs @@ -0,0 +1,145 @@ +use anyhow::{bail, Error}; + +use crate::{ + tape::changer::{ + MediaChange, + MtxStatus, + ElementStatus, + mtx_status, + mtx_load, + mtx_unload, + }, + api2::types::{ + ScsiTapeChanger, + LinuxTapeDrive, + }, +}; + +fn unload_to_free_slot(drive_name: &str, path: &str, status: &MtxStatus, drivenum: u64) -> Result<(), Error> { + + if drivenum >= status.drives.len() as u64 { + bail!("unload drive '{}' got unexpected drive number '{}' - changer only has '{}' drives", + drive_name, drivenum, status.drives.len()); + } + let drive_status = &status.drives[drivenum as usize]; + if let Some(slot) = drive_status.loaded_slot { + mtx_unload(path, slot, drivenum) + } else { + let mut free_slot = None; + for i in 0..status.slots.len() { + if let ElementStatus::Empty = status.slots[i] { + free_slot = Some((i+1) as u64); + break; + } + } + if let Some(slot) = free_slot { + mtx_unload(path, slot, drivenum) + } else { + bail!("drive '{}' unload failure - no free slot", drive_name); + } + } +} + +impl MediaChange for LinuxTapeDrive { + + fn load_media(&mut self, changer_id: &str) -> Result<(), Error> { + + if changer_id.starts_with("CLN") { + bail!("unable to load media '{}' (seems top be a a cleaning units)", changer_id); + } + + 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 status = mtx_status(&changer.path)?; + + let drivenum = 0; // fixme: read from drive config + + // already loaded? + for (i, drive_status) in status.drives.iter().enumerate() { + if let ElementStatus::VolumeTag(ref tag) = drive_status.status { + if *tag == changer_id { + if i != drivenum { + bail!("unable to load media '{}' - media in wrong drive ({} != {})", + changer_id, i, drivenum); + } + return Ok(()) + } + } + if i == drivenum { + match drive_status.status { + ElementStatus::Empty => { /* OK */ }, + _ => unload_to_free_slot(&self.name, &changer.path, &status, drivenum as u64)?, + } + } + } + + let mut slot = None; + for (i, element_status) in status.slots.iter().enumerate() { + if let ElementStatus::VolumeTag(tag) = element_status { + if *tag == changer_id { + slot = Some(i+1); + break; + } + } + } + + let slot = match slot { + None => bail!("unable to find media '{}' (offline?)", changer_id), + Some(slot) => slot, + }; + + + mtx_load(&changer.path, slot as u64, drivenum as u64) + } + + fn unload_media(&mut 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(()), + }; + + let drivenum: u64 = 0; + + let status = mtx_status(&changer.path)?; + + 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.path)?; + + 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 element_status in status.slots.iter() { + 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 new file mode 100644 index 00000000..aba728a3 --- /dev/null +++ b/src/tape/changer/mod.rs @@ -0,0 +1,35 @@ +mod email; +pub use email::*; + +mod parse_mtx_status; +pub use parse_mtx_status::*; + +mod mtx_wrapper; +pub use mtx_wrapper::*; + +mod linux_tape; +pub use linux_tape::*; + +use anyhow::Error; + +/// Interface to media change devices +pub trait MediaChange { + + /// Load media into drive + /// + /// This unloads first if the drive is already loaded with another media. + 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>; + + /// 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>; +} diff --git a/src/tape/changer/mtx_wrapper.rs b/src/tape/changer/mtx_wrapper.rs new file mode 100644 index 00000000..ae0ec099 --- /dev/null +++ b/src/tape/changer/mtx_wrapper.rs @@ -0,0 +1,84 @@ +use std::collections::HashSet; + +use anyhow::Error; + +use proxmox::tools::Uuid; + +use crate::{ + tools::run_command, + tape::{ + Inventory, + changer::{ + MtxStatus, + ElementStatus, + parse_mtx_status, + }, + }, +}; + +/// Run 'mtx status' and return parsed result. +pub fn mtx_status(path: &str) -> Result { + + let mut command = std::process::Command::new("mtx"); + command.args(&["-f", path, "status"]); + + let output = run_command(command, None)?; + + let status = parse_mtx_status(&output)?; + + Ok(status) +} + +/// Run 'mtx load' +pub fn mtx_load( + path: &str, + slot: u64, + drivenum: u64, +) -> Result<(), Error> { + + let mut command = std::process::Command::new("mtx"); + command.args(&["-f", path, "load", &slot.to_string(), &drivenum.to_string()]); + run_command(command, None)?; + + Ok(()) +} + +/// Run 'mtx unload' +pub fn mtx_unload( + path: &str, + slot: u64, + drivenum: u64, +) -> Result<(), Error> { + + let mut command = std::process::Command::new("mtx"); + command.args(&["-f", path, "unload", &slot.to_string(), &drivenum.to_string()]); + run_command(command, None)?; + + 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 slot_status in status.slots.iter() { + 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/changer/parse_mtx_status.rs b/src/tape/changer/parse_mtx_status.rs new file mode 100644 index 00000000..4b5a55c5 --- /dev/null +++ b/src/tape/changer/parse_mtx_status.rs @@ -0,0 +1,161 @@ +use anyhow::Error; + +use nom::{ + bytes::complete::{take_while, tag}, +}; + +use crate::tools::nom::{ + parse_complete, multispace0, multispace1, parse_u64, + parse_failure, parse_error, IResult, +}; + +pub enum ElementStatus { + Empty, + Full, + VolumeTag(String), +} + +pub struct DriveStatus { + pub loaded_slot: Option, + pub status: ElementStatus, +} + +pub struct MtxStatus { + pub drives: Vec, + pub slots: Vec, +} + +// Recognizes one line +fn next_line(i: &str) -> IResult<&str, &str> { + let (i, line) = take_while(|c| (c != '\n'))(i)?; + if i.is_empty() { + Ok((i, line)) + } else { + Ok((&i[1..], line)) + } +} + +fn parse_storage_changer(i: &str) -> IResult<&str, ()> { + + let (i, _) = multispace0(i)?; + let (i, _) = tag("Storage Changer")(i)?; + let (i, _) = next_line(i)?; // skip + + Ok((i, ())) +} + +fn parse_drive_status(i: &str) -> IResult<&str, DriveStatus> { + + let mut loaded_slot = None; + + if i.starts_with("Empty") { + return Ok((&i[5..], DriveStatus { loaded_slot, status: ElementStatus::Empty })); + } + let (mut i, _) = tag("Full (")(i)?; + + if i.starts_with("Storage Element ") { + let n = &i[16..]; + let (n, id) = parse_u64(n)?; + loaded_slot = Some(id); + let (n, _) = tag(" Loaded")(n)?; + i = n; + } else { + let (n, _) = take_while(|c| !(c == ')' || c == '\n'))(i)?; // skip to ')' + i = n; + } + + let (i, _) = tag(")")(i)?; + + if i.starts_with(":VolumeTag = ") { + let i = &i[13..]; + let (i, tag) = take_while(|c| !(c == ' ' || c == ':' || c == '\n'))(i)?; + let (i, _) = take_while(|c| c != '\n')(i)?; // skip to eol + return Ok((i, DriveStatus { loaded_slot, status: ElementStatus::VolumeTag(tag.to_string()) })); + } + + let (i, _) = take_while(|c| c != '\n')(i)?; // skip + + Ok((i, DriveStatus { loaded_slot, status: ElementStatus::Full })) +} + +fn parse_slot_status(i: &str) -> IResult<&str, ElementStatus> { + if i.starts_with("Empty") { + return Ok((&i[5..], ElementStatus::Empty)); + } + if i.starts_with("Full ") { + let mut n = &i[5..]; + + if n.starts_with(":VolumeTag=") { + n = &n[11..]; + let (n, tag) = take_while(|c| !(c == ' ' || c == ':' || c == '\n'))(n)?; + let (n, _) = take_while(|c| c != '\n')(n)?; // skip to eol + return Ok((n, ElementStatus::VolumeTag(tag.to_string()))); + + } + let (n, _) = take_while(|c| c != '\n')(n)?; // skip + + return Ok((n, ElementStatus::Full)); + } + + Err(parse_error(i, "unexptected element status")) +} + +fn parse_data_transfer_element(i: &str) -> IResult<&str, (u64, DriveStatus)> { + + let (i, _) = tag("Data Transfer Element")(i)?; + let (i, _) = multispace1(i)?; + let (i, id) = parse_u64(i)?; + let (i, _) = nom::character::complete::char(':')(i)?; + let (i, element_status) = parse_drive_status(i)?; + let (i, _) = nom::character::complete::newline(i)?; + + Ok((i, (id, element_status))) +} + +fn parse_storage_element(i: &str) -> IResult<&str, (u64, ElementStatus)> { + + let (i, _) = multispace1(i)?; + let (i, _) = tag("Storage Element")(i)?; + let (i, _) = multispace1(i)?; + let (i, id) = parse_u64(i)?; + let (i, _) = nom::character::complete::char(':')(i)?; + let (i, element_status) = parse_slot_status(i)?; + let (i, _) = nom::character::complete::newline(i)?; + + Ok((i, (id, element_status))) +} + +fn parse_status(i: &str) -> IResult<&str, MtxStatus> { + + let (mut i, _) = parse_storage_changer(i)?; + + let mut drives = Vec::new(); + while let Ok((n, (id, drive_status))) = parse_data_transfer_element(i) { + if id != drives.len() as u64 { + return Err(parse_failure(i, "unexpected drive number")); + } + i = n; + drives.push(drive_status); + } + + let mut slots = Vec::new(); + while let Ok((n, (id, element_status))) = parse_storage_element(i) { + if id != (slots.len() as u64 + 1) { + return Err(parse_failure(i, "unexpected slot number")); + } + i = n; + slots.push(element_status); + } + + let status = MtxStatus { drives, slots }; + + Ok((i, status)) +} + +/// Parses the output from 'mtx status' +pub fn parse_mtx_status(i: &str) -> Result { + + let status = parse_complete("mtx status", i, parse_status)?; + + Ok(status) +} diff --git a/src/tape/mod.rs b/src/tape/mod.rs index 05f4e2b4..9e11ea4e 100644 --- a/src/tape/mod.rs +++ b/src/tape/mod.rs @@ -9,6 +9,9 @@ pub use tape_read::*; mod inventory; pub use inventory::*; +mod changer; +pub use changer::*; + /// Directory path where we stora all status information pub const MEDIA_POOL_STATUS_DIR: &str = "/var/lib/proxmox-backup/mediapool";