tape: add tape changer support using 'mtx' command

This commit is contained in:
Dietmar Maurer 2020-12-05 14:46:57 +01:00
parent 7320e9ff4b
commit b107fdb99a
6 changed files with 485 additions and 0 deletions

57
src/tape/changer/email.rs Normal file
View File

@ -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<Vec<String>, Error> {
Ok(Vec::new())
}
}

View File

@ -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<Vec<String>, 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)
}
}

35
src/tape/changer/mod.rs Normal file
View File

@ -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<Vec<String>, Error>;
}

View File

@ -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<MtxStatus, Error> {
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<Uuid> {
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
}

View File

@ -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<u64>,
pub status: ElementStatus,
}
pub struct MtxStatus {
pub drives: Vec<DriveStatus>,
pub slots: Vec<ElementStatus>,
}
// 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<MtxStatus, Error> {
let status = parse_complete("mtx status", i, parse_status)?;
Ok(status)
}

View File

@ -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";