tape: add tape changer support using 'mtx' command
This commit is contained in:
parent
7320e9ff4b
commit
b107fdb99a
57
src/tape/changer/email.rs
Normal file
57
src/tape/changer/email.rs
Normal 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())
|
||||
}
|
||||
|
||||
}
|
145
src/tape/changer/linux_tape.rs
Normal file
145
src/tape/changer/linux_tape.rs
Normal 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
35
src/tape/changer/mod.rs
Normal 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>;
|
||||
}
|
84
src/tape/changer/mtx_wrapper.rs
Normal file
84
src/tape/changer/mtx_wrapper.rs
Normal 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
|
||||
}
|
161
src/tape/changer/parse_mtx_status.rs
Normal file
161
src/tape/changer/parse_mtx_status.rs
Normal 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)
|
||||
}
|
@ -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";
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user