Thomas Lamprecht d1d74c4367 typo fixes all over the place
found and semi-manually replaced by using:
 codespell -L mut -L crate -i 3 -w

Mostly in comments, but also email notification and two occurrences
of misspelled  'reserved' struct member, which where not used and
cargo build did not complain about the change, soo ...

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-03-10 16:39:57 +01:00

575 lines
17 KiB
Rust

//! Media changer implementation (SCSI media changer)
mod email;
pub use email::*;
pub mod sg_pt_changer;
pub mod mtx;
mod online_status_map;
pub use online_status_map::*;
use std::collections::HashSet;
use std::path::PathBuf;
use anyhow::{bail, Error};
use serde::{Serialize, Deserialize};
use serde_json::Value;
use proxmox::{
api::schema::parse_property_string,
tools::fs::{
CreateOptions,
replace_file,
file_read_optional_string,
},
};
use crate::api2::types::{
SLOT_ARRAY_SCHEMA,
ScsiTapeChanger,
LinuxTapeDrive,
};
/// Changer element status.
///
/// Drive and slots may be `Empty`, or contain some media, either
/// with known volume tag `VolumeTag(String)`, or without (`Full`).
#[derive(Serialize, Deserialize, Debug)]
pub enum ElementStatus {
Empty,
Full,
VolumeTag(String),
}
/// Changer drive status.
#[derive(Serialize, Deserialize)]
pub struct DriveStatus {
/// The slot the element was loaded from (if known).
pub loaded_slot: Option<u64>,
/// The status.
pub status: ElementStatus,
/// Drive Identifier (Serial number)
pub drive_serial_number: Option<String>,
/// Drive Vendor
pub vendor: Option<String>,
/// Drive Model
pub model: Option<String>,
/// Element Address
pub element_address: u16,
}
/// Storage element status.
#[derive(Serialize, Deserialize)]
pub struct StorageElementStatus {
/// Flag for Import/Export slots
pub import_export: bool,
/// The status.
pub status: ElementStatus,
/// Element Address
pub element_address: u16,
}
/// Transport element status.
#[derive(Serialize, Deserialize)]
pub struct TransportElementStatus {
/// The status.
pub status: ElementStatus,
/// Element Address
pub element_address: u16,
}
/// Changer status - show drive/slot usage
#[derive(Serialize, Deserialize)]
pub struct MtxStatus {
/// List of known drives
pub drives: Vec<DriveStatus>,
/// List of known storage slots
pub slots: Vec<StorageElementStatus>,
/// Transport elements
///
/// Note: Some libraries do not report transport elements.
pub transports: Vec<TransportElementStatus>,
}
impl MtxStatus {
pub fn slot_address(&self, slot: u64) -> Result<u16, Error> {
if slot == 0 {
bail!("invalid slot number '{}' (slots numbers starts at 1)", slot);
}
if slot > (self.slots.len() as u64) {
bail!("invalid slot number '{}' (max {} slots)", slot, self.slots.len());
}
Ok(self.slots[(slot -1) as usize].element_address)
}
pub fn drive_address(&self, drivenum: u64) -> Result<u16, Error> {
if drivenum >= (self.drives.len() as u64) {
bail!("invalid drive number '{}'", drivenum);
}
Ok(self.drives[drivenum as usize].element_address)
}
pub fn transport_address(&self) -> u16 {
// simply use first transport
// (are there changers exposing more than one?)
// defaults to 0 for changer that do not report transports
self
.transports
.get(0)
.map(|t| t.element_address)
.unwrap_or(0u16)
}
pub fn find_free_slot(&self, import_export: bool) -> Option<u64> {
let mut free_slot = None;
for (i, slot_info) in self.slots.iter().enumerate() {
if slot_info.import_export != import_export {
continue; // skip slots of wrong type
}
if let ElementStatus::Empty = slot_info.status {
free_slot = Some((i+1) as u64);
break;
}
}
free_slot
}
pub fn mark_import_export_slots(&mut self, config: &ScsiTapeChanger) -> Result<(), Error>{
let mut export_slots: HashSet<u64> = HashSet::new();
if let Some(slots) = &config.export_slots {
let slots: Value = parse_property_string(&slots, &SLOT_ARRAY_SCHEMA)?;
export_slots = slots
.as_array()
.unwrap()
.iter()
.filter_map(|v| v.as_u64())
.collect();
}
for (i, entry) in self.slots.iter_mut().enumerate() {
let slot = i as u64 + 1;
if export_slots.contains(&slot) {
entry.import_export = true; // mark as IMPORT/EXPORT
}
}
Ok(())
}
}
/// 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>;
fn unload(&mut self, to_slot: u64, drivenum: u64) -> Result<MtxStatus, Error>;
fn transfer(&mut self, from_slot: u64, to_slot: u64) -> Result<MtxStatus, Error>;
}
/// Interface to the media changer device for a single drive
pub trait MediaChange {
/// Drive number inside changer
fn drive_number(&self) -> u64;
/// Drive name (used for debug messages)
fn drive_name(&self) -> &str;
/// Returns the changer status
fn status(&mut self) -> Result<MtxStatus, Error>;
/// Transfer media from on slot to another (storage or import export slots)
///
/// Target slot needs to be empty
fn transfer_media(&mut self, from: u64, to: u64) -> Result<MtxStatus, Error>;
/// Load media from storage slot into drive
fn load_media_from_slot(&mut self, slot: u64) -> Result<MtxStatus, Error>;
/// Load media by label-text into drive
///
/// This unloads first if the drive is already loaded with another media.
///
/// Note: This refuses to load media inside import/export
/// 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);
}
let mut status = self.status()?;
let mut unload_drive = false;
// already loaded?
for (i, drive_status) in status.drives.iter().enumerate() {
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());
}
return Ok(status) // already loaded
}
}
if i as u64 == self.drive_number() {
match drive_status.status {
ElementStatus::Empty => { /* OK */ },
_ => unload_drive = true,
}
}
}
if unload_drive {
status = self.unload_to_free_slot(status)?;
}
let mut slot = None;
for (i, slot_info) in status.slots.iter().enumerate() {
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);
}
slot = Some(i+1);
break;
}
}
}
let slot = match slot {
None => bail!("unable to find media '{}' (offline?)", label_text),
Some(slot) => slot,
};
self.load_media_from_slot(slot as u64)
}
/// Unload media from drive (eject media if necessary)
fn unload_media(&mut self, target_slot: Option<u64>) -> Result<MtxStatus, Error>;
/// List online media labels (label_text/barcodes)
///
/// List accessible (online) label texts. This does not include
/// media inside import-export slots or cleaning media.
fn online_media_label_texts(&mut self) -> Result<Vec<String>, 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 slot_info in status.slots.iter() {
if slot_info.import_export { continue; }
if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
if tag.starts_with("CLN") { continue; }
list.push(tag.clone());
}
}
Ok(list)
}
/// Load/Unload cleaning cartridge
///
/// This fail if there is no cleaning cartridge online. Any media
/// inside the drive is automatically unloaded.
fn clean_drive(&mut self) -> Result<MtxStatus, Error> {
let mut status = self.status()?;
// 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)?; }
}
}
let mut cleaning_cartridge_slot = None;
for (i, slot_info) in status.slots.iter().enumerate() {
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);
break;
}
}
}
let cleaning_cartridge_slot = match cleaning_cartridge_slot {
None => bail!("clean failed - unable to find cleaning cartridge"),
Some(cleaning_cartridge_slot) => cleaning_cartridge_slot as u64,
};
self.load_media_from_slot(cleaning_cartridge_slot)?;
self.unload_media(Some(cleaning_cartridge_slot))
}
/// Export media
///
/// By moving the media to an empty import-export slot. Returns
/// Some(slot) if the media was exported. Returns None if the media is
/// not online (already exported).
fn export_media(&mut self, label_text: &str) -> Result<Option<u64>, Error> {
let status = self.status()?;
let mut unload_from_drive = false;
if let Some(drive_status) = status.drives.get(self.drive_number() as usize) {
if let ElementStatus::VolumeTag(ref tag) = drive_status.status {
if tag == label_text {
unload_from_drive = true;
}
}
}
let mut from = None;
let mut to = None;
for (i, slot_info) in status.slots.iter().enumerate() {
if slot_info.import_export {
if to.is_some() { continue; }
if let ElementStatus::Empty = slot_info.status {
to = Some(i as u64 + 1);
}
} else if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
if tag == label_text {
from = Some(i as u64 + 1);
}
}
}
if unload_from_drive {
match to {
Some(to) => {
self.unload_media(Some(to))?;
Ok(Some(to))
}
None => bail!("unable to find free export slot"),
}
} else {
match (from, to) {
(Some(from), Some(to)) => {
self.transfer_media(from, to)?;
Ok(Some(to))
}
(Some(_from), None) => bail!("unable to find free export slot"),
(None, _) => Ok(None), // not online
}
}
}
/// Unload media to a free storage slot
///
/// If possible to the slot it was previously loaded from.
///
/// 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
if let Some(info) = status.slots.get(slot as usize - 1) {
if let ElementStatus::Empty = info.status {
return self.unload_media(Some(slot));
}
}
}
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());
}
}
}
const USE_MTX: bool = false;
impl ScsiMediaChange for ScsiTapeChanger {
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);
}
}
let status = if USE_MTX {
mtx::mtx_status(&self)
} else {
sg_pt_changer::status(&self)
};
match &status {
Ok(status) => {
save_changer_state_cache(&self.name, status)?;
}
Err(_) => {
delete_changer_state_cache(&self.name);
}
}
status
}
fn load_slot(&mut self, from_slot: u64, drivenum: u64) -> Result<MtxStatus, Error> {
let result = if USE_MTX {
mtx::mtx_load(&self.path, from_slot, drivenum)
} else {
let mut file = sg_pt_changer::open(&self.path)?;
sg_pt_changer::load_slot(&mut file, from_slot, drivenum)
};
let status = self.status(false)?; // always update status
result?; // check load result
Ok(status)
}
fn unload(&mut self, to_slot: u64, drivenum: u64) -> Result<MtxStatus, Error> {
let result = if USE_MTX {
mtx::mtx_unload(&self.path, to_slot, drivenum)
} else {
let mut file = sg_pt_changer::open(&self.path)?;
sg_pt_changer::unload(&mut file, to_slot, drivenum)
};
let status = self.status(false)?; // always update status
result?; // check unload result
Ok(status)
}
fn transfer(&mut self, from_slot: u64, to_slot: u64) -> Result<MtxStatus, Error> {
let result = if USE_MTX {
mtx::mtx_transfer(&self.path, from_slot, to_slot)
} else {
let mut file = sg_pt_changer::open(&self.path)?;
sg_pt_changer::transfer_medium(&mut file, from_slot, to_slot)
};
let status = self.status(false)?; // always update status
result?; // check unload result
Ok(status)
}
}
fn save_changer_state_cache(
changer: &str,
state: &MtxStatus,
) -> Result<(), Error> {
let mut path = PathBuf::from(crate::tape::CHANGER_STATE_DIR);
path.push(changer);
let state = serde_json::to_string_pretty(state)?;
let backup_user = crate::backup::backup_user()?;
let mode = nix::sys::stat::Mode::from_bits_truncate(0o0644);
let options = CreateOptions::new()
.perm(mode)
.owner(backup_user.uid)
.group(backup_user.gid);
replace_file(path, state.as_bytes(), options)
}
fn delete_changer_state_cache(changer: &str) {
let mut path = PathBuf::from("/run/proxmox-backup/changer-state");
path.push(changer);
let _ = std::fs::remove_file(&path); // ignore errors
}
fn load_changer_state_cache(changer: &str) -> Result<Option<MtxStatus>, Error> {
let mut path = PathBuf::from("/run/proxmox-backup/changer-state");
path.push(changer);
let data = match file_read_optional_string(&path)? {
None => return Ok(None),
Some(data) => data,
};
let state = serde_json::from_str(&data)?;
Ok(Some(state))
}
/// Implements MediaChange using 'mtx' linux cli tool
pub struct MtxMediaChanger {
drive_name: String, // used for error messages
drive_number: u64,
config: ScsiTapeChanger,
}
impl MtxMediaChanger {
pub fn with_drive_config(drive_config: &LinuxTapeDrive) -> Result<Self, Error> {
let (config, _digest) = crate::config::drive::config()?;
let changer_config: ScsiTapeChanger = match drive_config.changer {
Some(ref changer) => config.lookup("changer", changer)?,
None => bail!("drive '{}' has no associated changer", drive_config.name),
};
Ok(Self {
drive_name: drive_config.name.clone(),
drive_number: drive_config.changer_drivenum.unwrap_or(0),
config: changer_config,
})
}
}
impl MediaChange for MtxMediaChanger {
fn drive_number(&self) -> u64 {
self.drive_number
}
fn drive_name(&self) -> &str {
&self.drive_name
}
fn status(&mut self) -> Result<MtxStatus, Error> {
self.config.status(false)
}
fn transfer_media(&mut self, from: u64, to: u64) -> Result<MtxStatus, Error> {
self.config.transfer(from, to)
}
fn load_media_from_slot(&mut self, slot: u64) -> Result<MtxStatus, Error> {
self.config.load_slot(slot, self.drive_number)
}
fn unload_media(&mut self, target_slot: Option<u64>) -> Result<MtxStatus, Error> {
if let Some(target_slot) = target_slot {
self.config.unload(target_slot, self.drive_number)
} else {
let status = self.status()?;
self.unload_to_free_slot(status)
}
}
}