tape: add/use rust scsi changer implementation using libsgutil2

This commit is contained in:
Dietmar Maurer 2021-01-25 10:15:59 +01:00
parent a2379996e6
commit 697c41c584
11 changed files with 918 additions and 153 deletions

View File

@ -36,4 +36,3 @@ Chores:
Suggestions Suggestions
=========== ===========
* tape: rewrite mtx in Rust

View File

@ -22,9 +22,8 @@ use crate::{
changer::{ changer::{
OnlineStatusMap, OnlineStatusMap,
ElementStatus, ElementStatus,
mtx_status, ScsiMediaChange,
mtx_status_to_online_set, mtx_status_to_online_set,
mtx_transfer,
}, },
}, },
}; };
@ -51,10 +50,10 @@ pub async fn get_status(name: String) -> Result<Vec<MtxStatusEntry>, Error> {
let (config, _digest) = config::drive::config()?; let (config, _digest) = config::drive::config()?;
let data: ScsiTapeChanger = config.lookup("changer", &name)?; let mut changer_config: ScsiTapeChanger = config.lookup("changer", &name)?;
let status = tokio::task::spawn_blocking(move || { let status = tokio::task::spawn_blocking(move || {
mtx_status(&data) changer_config.status()
}).await??; }).await??;
let state_path = Path::new(TAPE_STATUS_DIR); let state_path = Path::new(TAPE_STATUS_DIR);
@ -82,15 +81,15 @@ pub async fn get_status(name: String) -> Result<Vec<MtxStatusEntry>, Error> {
list.push(entry); list.push(entry);
} }
for (id, (import_export, slot_status)) in status.slots.iter().enumerate() { for (id, slot_info) in status.slots.iter().enumerate() {
let entry = MtxStatusEntry { let entry = MtxStatusEntry {
entry_kind: if *import_export { entry_kind: if slot_info.import_export {
MtxEntryKind::ImportExport MtxEntryKind::ImportExport
} else { } else {
MtxEntryKind::Slot MtxEntryKind::Slot
}, },
entry_id: id as u64 + 1, entry_id: id as u64 + 1,
label_text: match &slot_status { label_text: match &slot_info.status {
ElementStatus::Empty => None, ElementStatus::Empty => None,
ElementStatus::Full => Some(String::new()), ElementStatus::Full => Some(String::new()),
ElementStatus::VolumeTag(tag) => Some(tag.to_string()), ElementStatus::VolumeTag(tag) => Some(tag.to_string()),
@ -129,10 +128,10 @@ pub async fn transfer(
let (config, _digest) = config::drive::config()?; let (config, _digest) = config::drive::config()?;
let data: ScsiTapeChanger = config.lookup("changer", &name)?; let mut changer_config: ScsiTapeChanger = config.lookup("changer", &name)?;
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
mtx_transfer(&data.path, from, to) changer_config.transfer(from, to)
}).await? }).await?
} }

View File

@ -3,20 +3,117 @@
mod email; mod email;
pub use email::*; pub use email::*;
mod parse_mtx_status; pub mod sg_pt_changer;
pub use parse_mtx_status::*;
mod mtx_wrapper; pub mod mtx;
pub use mtx_wrapper::*;
mod mtx;
pub use mtx::*;
mod online_status_map; mod online_status_map;
pub use online_status_map::*; pub use online_status_map::*;
use anyhow::{bail, Error}; use anyhow::{bail, Error};
use crate::api2::types::{
ScsiTapeChanger,
LinuxTapeDrive,
};
/// Changer element status.
///
/// Drive and slots may be `Empty`, or contain some media, either
/// with knwon volume tag `VolumeTag(String)`, or without (`Full`).
pub enum ElementStatus {
Empty,
Full,
VolumeTag(String),
}
/// Changer drive status.
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>,
/// Element Address
pub element_address: u16,
}
/// Storage element status.
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.
pub struct TransportElementStatus {
/// The status.
pub status: ElementStatus,
/// Element Address
pub element_address: u16,
}
/// Changer status - show drive/slot usage
pub struct MtxStatus {
/// List of known drives
pub drives: Vec<DriveStatus>,
/// List of known storage slots
pub slots: Vec<StorageElementStatus>,
/// Tranport 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)
}
}
/// Interface to SCSI changer devices
pub trait ScsiMediaChange {
fn status(&mut self) -> Result<MtxStatus, Error>;
fn load_slot(&mut self, from_slot: u64, drivenum: u64) -> Result<(), Error>;
fn unload(&mut self, to_slot: u64, drivenum: u64) -> Result<(), Error>;
fn transfer(&mut self, from_slot: u64, to_slot: u64) -> Result<(), Error>;
}
/// Interface to the media changer device for a single drive /// Interface to the media changer device for a single drive
pub trait MediaChange { pub trait MediaChange {
@ -79,10 +176,10 @@ pub trait MediaChange {
} }
let mut slot = None; let mut slot = None;
for (i, (import_export, element_status)) in status.slots.iter().enumerate() { for (i, slot_info) in status.slots.iter().enumerate() {
if let ElementStatus::VolumeTag(tag) = element_status { if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
if *tag == label_text { if tag == label_text {
if *import_export { if slot_info.import_export {
bail!("unable to load media '{}' - inside import/export slot", label_text); bail!("unable to load media '{}' - inside import/export slot", label_text);
} }
slot = Some(i+1); slot = Some(i+1);
@ -117,9 +214,9 @@ pub trait MediaChange {
} }
} }
for (import_export, element_status) in status.slots.iter() { for slot_info in status.slots.iter() {
if *import_export { continue; } if slot_info.import_export { continue; }
if let ElementStatus::VolumeTag(ref tag) = element_status { if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
if tag.starts_with("CLN") { continue; } if tag.starts_with("CLN") { continue; }
list.push(tag.clone()); list.push(tag.clone());
} }
@ -137,9 +234,9 @@ pub trait MediaChange {
let mut cleaning_cartridge_slot = None; let mut cleaning_cartridge_slot = None;
for (i, (import_export, element_status)) in status.slots.iter().enumerate() { for (i, slot_info) in status.slots.iter().enumerate() {
if *import_export { continue; } if slot_info.import_export { continue; }
if let ElementStatus::VolumeTag(ref tag) = element_status { if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
if tag.starts_with("CLN") { if tag.starts_with("CLN") {
cleaning_cartridge_slot = Some(i + 1); cleaning_cartridge_slot = Some(i + 1);
break; break;
@ -186,13 +283,13 @@ pub trait MediaChange {
let mut from = None; let mut from = None;
let mut to = None; let mut to = None;
for (i, (import_export, element_status)) in status.slots.iter().enumerate() { for (i, slot_info) in status.slots.iter().enumerate() {
if *import_export { if slot_info.import_export {
if to.is_some() { continue; } if to.is_some() { continue; }
if let ElementStatus::Empty = element_status { if let ElementStatus::Empty = slot_info.status {
to = Some(i as u64 + 1); to = Some(i as u64 + 1);
} }
} else if let ElementStatus::VolumeTag(ref tag) = element_status { } else if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
if tag == label_text { if tag == label_text {
from = Some(i as u64 + 1); from = Some(i as u64 + 1);
} }
@ -230,7 +327,7 @@ pub trait MediaChange {
if let Some(slot) = drive_status.loaded_slot { if let Some(slot) = drive_status.loaded_slot {
// check if original slot is empty/usable // check if original slot is empty/usable
if let Some(info) = status.slots.get(slot as usize - 1) { if let Some(info) = status.slots.get(slot as usize - 1) {
if let (_import_export, ElementStatus::Empty) = info { if let ElementStatus::Empty = info.status {
return self.unload_media(Some(slot)); return self.unload_media(Some(slot));
} }
} }
@ -238,8 +335,8 @@ pub trait MediaChange {
let mut free_slot = None; let mut free_slot = None;
for i in 0..status.slots.len() { for i in 0..status.slots.len() {
if status.slots[i].0 { continue; } // skip import/export slots if status.slots[i].import_export { continue; } // skip import/export slots
if let ElementStatus::Empty = status.slots[i].1 { if let ElementStatus::Empty = status.slots[i].status {
free_slot = Some((i+1) as u64); free_slot = Some((i+1) as u64);
break; break;
} }
@ -251,3 +348,100 @@ pub trait MediaChange {
} }
} }
} }
const USE_MTX: bool = false;
impl ScsiMediaChange for ScsiTapeChanger {
fn status(&mut self) -> Result<MtxStatus, Error> {
if USE_MTX {
mtx::mtx_status(&self)
} else {
let mut file = sg_pt_changer::open(&self.path)?;
sg_pt_changer::read_element_status(&mut file)
}
}
fn load_slot(&mut self, from_slot: u64, drivenum: u64) -> Result<(), Error> {
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)
}
}
fn unload(&mut self, to_slot: u64, drivenum: u64) -> Result<(), Error> {
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)
}
}
fn transfer(&mut self, from_slot: u64, to_slot: u64) -> Result<(), Error> {
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)
}
}
}
/// 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_drive_id.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()
}
fn transfer_media(&mut self, from: u64, to: u64) -> Result<(), Error> {
self.config.transfer(from, to)
}
fn load_media_from_slot(&mut self, slot: u64) -> Result<(), Error> {
self.config.load_slot(slot, self.drive_number)
}
fn unload_media(&mut self, target_slot: Option<u64>) -> Result<(), 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)
}
}
}

View File

@ -1,72 +0,0 @@
use anyhow::{bail, Error};
use crate::{
tape::changer::{
MediaChange,
MtxStatus,
mtx_status,
mtx_transfer,
mtx_load,
mtx_unload,
},
api2::types::{
ScsiTapeChanger,
LinuxTapeDrive,
},
};
/// 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_drive_id.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> {
mtx_status(&self.config)
}
fn transfer_media(&mut self, from: u64, to: u64) -> Result<(), Error> {
mtx_transfer(&self.config.path, from, to)
}
fn load_media_from_slot(&mut self, slot: u64) -> Result<(), Error> {
mtx_load(&self.config.path, slot, self.drive_number)
}
fn unload_media(&mut self, target_slot: Option<u64>) -> Result<(), Error> {
if let Some(target_slot) = target_slot {
mtx_unload(&self.config.path, target_slot, self.drive_number)
} else {
let status = self.status()?;
self.unload_to_free_slot(status)
}
}
}

View File

@ -0,0 +1,7 @@
//! Wrapper around expernal `mtx` command line tool
mod parse_mtx_status;
pub use parse_mtx_status::*;
mod mtx_wrapper;
pub use mtx_wrapper::*;

View File

@ -16,9 +16,11 @@ use crate::{
tape::{ tape::{
changer::{ changer::{
MtxStatus, MtxStatus,
mtx::{
parse_mtx_status, parse_mtx_status,
}, },
}, },
},
}; };
/// Run 'mtx status' and return parsed result. /// Run 'mtx status' and return parsed result.
@ -48,7 +50,7 @@ pub fn mtx_status(config: &ScsiTapeChanger) -> Result<MtxStatus, Error> {
for (i, entry) in status.slots.iter_mut().enumerate() { for (i, entry) in status.slots.iter_mut().enumerate() {
let slot = i as u64 + 1; let slot = i as u64 + 1;
if export_slots.contains(&slot) { if export_slots.contains(&slot) {
entry.0 = true; // mark as IMPORT/EXPORT entry.import_export = true; // mark as IMPORT/EXPORT
} }
} }

View File

@ -4,36 +4,19 @@ use nom::{
bytes::complete::{take_while, tag}, bytes::complete::{take_while, tag},
}; };
use crate::tools::nom::{ use crate::{
tools::nom::{
parse_complete, multispace0, multispace1, parse_u64, parse_complete, multispace0, multispace1, parse_u64,
parse_failure, parse_error, IResult, parse_failure, parse_error, IResult,
},
tape::changer::{
ElementStatus,
MtxStatus,
DriveStatus,
StorageElementStatus,
},
}; };
/// Changer element status.
///
/// Drive and slots may be `Empty`, or contain some media, either
/// with knwon volume tag `VolumeTag(String)`, or without (`Full`).
pub enum ElementStatus {
Empty,
Full,
VolumeTag(String),
}
/// Changer drive status.
pub struct DriveStatus {
/// The slot the element was loaded from (if known).
pub loaded_slot: Option<u64>,
/// The status.
pub status: ElementStatus,
}
/// Changer status - show drive/slot usage
pub struct MtxStatus {
/// List of known drives
pub drives: Vec<DriveStatus>,
/// List of known slots, the boolean attribute marks import/export slots
pub slots: Vec<(bool, ElementStatus)>,
}
// Recognizes one line // Recognizes one line
fn next_line(i: &str) -> IResult<&str, &str> { fn next_line(i: &str) -> IResult<&str, &str> {
@ -54,12 +37,18 @@ fn parse_storage_changer(i: &str) -> IResult<&str, ()> {
Ok((i, ())) Ok((i, ()))
} }
fn parse_drive_status(i: &str) -> IResult<&str, DriveStatus> { fn parse_drive_status(i: &str, id: u64) -> IResult<&str, DriveStatus> {
let mut loaded_slot = None; let mut loaded_slot = None;
if let Some(empty) = i.strip_prefix("Empty") { if let Some(empty) = i.strip_prefix("Empty") {
return Ok((empty, DriveStatus { loaded_slot, status: ElementStatus::Empty })); let status = DriveStatus {
loaded_slot,
status: ElementStatus::Empty,
drive_serial_number: None,
element_address: id as u16,
};
return Ok((empty, status));
} }
let (mut i, _) = tag("Full (")(i)?; let (mut i, _) = tag("Full (")(i)?;
@ -78,12 +67,24 @@ fn parse_drive_status(i: &str) -> IResult<&str, DriveStatus> {
if let Some(i) = i.strip_prefix(":VolumeTag = ") { if let Some(i) = i.strip_prefix(":VolumeTag = ") {
let (i, tag) = take_while(|c| !(c == ' ' || c == ':' || c == '\n'))(i)?; let (i, tag) = take_while(|c| !(c == ' ' || c == ':' || c == '\n'))(i)?;
let (i, _) = take_while(|c| c != '\n')(i)?; // skip to eol let (i, _) = take_while(|c| c != '\n')(i)?; // skip to eol
return Ok((i, DriveStatus { loaded_slot, status: ElementStatus::VolumeTag(tag.to_string()) })); let status = DriveStatus {
loaded_slot,
status: ElementStatus::VolumeTag(tag.to_string()),
drive_serial_number: None,
element_address: id as u16,
};
return Ok((i, status));
} }
let (i, _) = take_while(|c| c != '\n')(i)?; // skip let (i, _) = take_while(|c| c != '\n')(i)?; // skip
Ok((i, DriveStatus { loaded_slot, status: ElementStatus::Full })) let status = DriveStatus {
loaded_slot,
status: ElementStatus::Full,
drive_serial_number: None,
element_address: id as u16,
};
Ok((i, status))
} }
fn parse_slot_status(i: &str) -> IResult<&str, ElementStatus> { fn parse_slot_status(i: &str) -> IResult<&str, ElementStatus> {
@ -111,7 +112,7 @@ fn parse_data_transfer_element(i: &str) -> IResult<&str, (u64, DriveStatus)> {
let (i, _) = multispace1(i)?; let (i, _) = multispace1(i)?;
let (i, id) = parse_u64(i)?; let (i, id) = parse_u64(i)?;
let (i, _) = nom::character::complete::char(':')(i)?; let (i, _) = nom::character::complete::char(':')(i)?;
let (i, element_status) = parse_drive_status(i)?; let (i, element_status) = parse_drive_status(i, id)?;
let (i, _) = nom::character::complete::newline(i)?; let (i, _) = nom::character::complete::newline(i)?;
Ok((i, (id, element_status))) Ok((i, (id, element_status)))
@ -151,10 +152,15 @@ fn parse_status(i: &str) -> IResult<&str, MtxStatus> {
return Err(parse_failure(i, "unexpected slot number")); return Err(parse_failure(i, "unexpected slot number"));
} }
i = n; i = n;
slots.push((import_export, element_status)); let status = StorageElementStatus {
import_export,
status: element_status,
element_address: id as u16,
};
slots.push(status);
} }
let status = MtxStatus { drives, slots }; let status = MtxStatus { drives, slots, transports: Vec::new() };
Ok((i, status)) Ok((i, status))
} }

View File

@ -17,7 +17,7 @@ use crate::{
MediaChange, MediaChange,
MtxStatus, MtxStatus,
ElementStatus, ElementStatus,
mtx_status, mtx::mtx_status,
}, },
}, },
}; };
@ -108,9 +108,9 @@ pub fn mtx_status_to_online_set(status: &MtxStatus, inventory: &Inventory) -> Ha
} }
} }
for (import_export, slot_status) in status.slots.iter() { for slot_info in status.slots.iter() {
if *import_export { continue; } if slot_info.import_export { continue; }
if let ElementStatus::VolumeTag(ref label_text) = slot_status { if let ElementStatus::VolumeTag(ref label_text) = slot_info.status {
if let Some(media_id) = inventory.find_media_by_label_text(label_text) { if let Some(media_id) = inventory.find_media_by_label_text(label_text) {
online_set.insert(media_id.label.uuid.clone()); online_set.insert(media_id.label.uuid.clone());
} }

View File

@ -0,0 +1,618 @@
//! SCSI changer implementation using libsgutil2
use std::os::unix::prelude::AsRawFd;
use std::io::Read;
use std::collections::HashMap;
use std::path::Path;
use std::fs::{OpenOptions, File};
use anyhow::{bail, format_err, Error};
use endian_trait::Endian;
use proxmox::tools::io::ReadExt;
use crate::{
tape::{
changer::{
DriveStatus,
ElementStatus,
StorageElementStatus,
TransportElementStatus,
MtxStatus,
},
},
tools::sgutils2::{
SgRaw,
InquiryInfo,
scsi_ascii_to_string,
scsi_inquiry,
},
};
const SCSI_CHANGER_DEFAULT_TIMEOUT: usize = 60*5; // 5 minutes
/// Initialize element status (Inventory)
pub fn initialize_element_status<F: AsRawFd>(file: &mut F) -> Result<(), Error> {
let mut sg_raw = SgRaw::new(file, 64)?;
// like mtx(1), set a very long timeout (30 minutes)
sg_raw.set_timeout(30*60);
let mut cmd = Vec::new();
cmd.extend(&[0x07, 0, 0, 0, 0, 0]); // INITIALIZE ELEMENT STATUS (07h)
sg_raw.do_command(&cmd)
.map_err(|err| format_err!("initializte element status (07h) failed - {}", err))?;
Ok(())
}
#[repr(C, packed)]
#[derive(Endian)]
struct AddressAssignmentPage {
data_len: u8,
reserved1: u8,
reserved2: u8,
block_descriptor_len: u8,
page_code: u8,
additional_page_len: u8,
first_transport_element_address: u16,
transport_element_count: u16,
first_storage_element_address: u16,
storage_element_count: u16,
first_import_export_element_address: u16,
import_export_element_count: u16,
first_tranfer_element_address: u16,
transfer_element_count: u16,
reserved22: u8,
reserved23: u8,
}
fn read_element_address_assignment<F: AsRawFd>(
file: &mut F,
) -> Result<AddressAssignmentPage, Error> {
let allocation_len: u8 = u8::MAX;
let mut sg_raw = SgRaw::new(file, allocation_len as usize)?;
sg_raw.set_timeout(SCSI_CHANGER_DEFAULT_TIMEOUT);
let mut cmd = Vec::new();
cmd.push(0x1A); // MODE SENSE6 (1Ah)
cmd.push(0x08); // DBD=1 (The Disable Block Descriptors)
cmd.push(0x1D); // Element Address Assignment Page
cmd.push(0);
cmd.push(allocation_len); // allocation len
cmd.push(0); //control
let data = sg_raw.do_command(&cmd)
.map_err(|err| format_err!("read element address assignment failed - {}", err))?;
proxmox::try_block!({
let mut reader = &data[..];
let page: AddressAssignmentPage = unsafe { reader.read_be_value()? };
if page.data_len != 23 {
bail!("got unexpected page len ({} != 23)", page.data_len);
}
Ok(page)
}).map_err(|err: Error| format_err!("decode element address assignment page failed - {}", err))
}
fn scsi_move_medium_cdb(
medium_transport_address: u16,
source_element_address: u16,
destination_element_address: u16,
) -> Vec<u8> {
let mut cmd = Vec::new();
cmd.push(0xA5); // MOVE MEDIUM (A5h)
cmd.push(0); // reserved
cmd.extend(&medium_transport_address.to_be_bytes());
cmd.extend(&source_element_address.to_be_bytes());
cmd.extend(&destination_element_address.to_be_bytes());
cmd.push(0); // reserved
cmd.push(0); // reserved
cmd.push(0); // Invert=0
cmd.push(0); // control
cmd
}
/// Load media from storage slot into drive
pub fn load_slot(
file: &mut File,
from_slot: u64,
drivenum: u64,
) -> Result<(), Error> {
let status = read_element_status(file)?;
let transport_address = status.transport_address();
let source_element_address = status.slot_address(from_slot)?;
let drive_element_address = status.drive_address(drivenum)?;
let cmd = scsi_move_medium_cdb(
transport_address,
source_element_address,
drive_element_address,
);
let mut sg_raw = SgRaw::new(file, 64)?;
sg_raw.set_timeout(SCSI_CHANGER_DEFAULT_TIMEOUT);
sg_raw.do_command(&cmd)
.map_err(|err| format_err!("load drive failed - {}", err))?;
Ok(())
}
/// Unload media from drive into a storage slot
pub fn unload(
file: &mut File,
to_slot: u64,
drivenum: u64,
) -> Result<(), Error> {
let status = read_element_status(file)?;
let transport_address = status.transport_address();
let target_element_address = status.slot_address(to_slot)?;
let drive_element_address = status.drive_address(drivenum)?;
let cmd = scsi_move_medium_cdb(
transport_address,
drive_element_address,
target_element_address,
);
let mut sg_raw = SgRaw::new(file, 64)?;
sg_raw.set_timeout(SCSI_CHANGER_DEFAULT_TIMEOUT);
sg_raw.do_command(&cmd)
.map_err(|err| format_err!("unload drive failed - {}", err))?;
Ok(())
}
/// Tranfer medium from one storage slot to another
pub fn transfer_medium<F: AsRawFd>(
file: &mut F,
from_slot: u64,
to_slot: u64,
) -> Result<(), Error> {
let status = read_element_status(file)?;
let transport_address = status.transport_address();
let source_element_address = status.slot_address(from_slot)?;
let target_element_address = status.slot_address(to_slot)?;
let cmd = scsi_move_medium_cdb(
transport_address,
source_element_address,
target_element_address,
);
let mut sg_raw = SgRaw::new(file, 64)?;
sg_raw.set_timeout(SCSI_CHANGER_DEFAULT_TIMEOUT);
sg_raw.do_command(&cmd)
.map_err(|err| {
format_err!("transfer medium from slot {} to slot {} failed - {}",
from_slot, to_slot, err)
})?;
Ok(())
}
fn scsi_read_element_status_cdb(
start_element_address: u16,
allocation_len: u32,
) -> Vec<u8> {
let mut cmd = Vec::new();
cmd.push(0xB8); // READ ELEMENT STATUS (B8h)
cmd.push(1u8<<4); // report all types and volume tags
cmd.extend(&start_element_address.to_be_bytes());
let number_of_elements: u16 = 0xffff;
cmd.extend(&number_of_elements.to_be_bytes());
cmd.push(0b001); // Mixed=0,CurData=0,DVCID=1
cmd.extend(&allocation_len.to_be_bytes()[1..4]);
cmd.push(0);
cmd.push(0);
cmd
}
/// Read element status.
pub fn read_element_status<F: AsRawFd>(file: &mut F) -> Result<MtxStatus, Error> {
let inquiry = scsi_inquiry(file)?;
if inquiry.peripheral_type != 8 {
bail!("wrong device type (not a scsi changer device)");
}
// first, request address assignment (used for sanity checks)
let setup = read_element_address_assignment(file)?;
let allocation_len: u32 = 0x10000;
let mut sg_raw = SgRaw::new(file, allocation_len as usize)?;
sg_raw.set_timeout(SCSI_CHANGER_DEFAULT_TIMEOUT);
let mut start_element_address = 0;
let mut drives = Vec::new();
let mut storage_slots = Vec::new();
let mut import_export_slots = Vec::new();
let mut transports = Vec::new();
loop {
let cmd = scsi_read_element_status_cdb(start_element_address, allocation_len);
let data = sg_raw.do_command(&cmd)
.map_err(|err| format_err!("read element status (B8h) failed - {}", err))?;
let page = decode_element_status_page(&inquiry, data, start_element_address)?;
transports.extend(page.transports);
drives.extend(page.drives);
storage_slots.extend(page.storage_slots);
import_export_slots.extend(page.import_export_slots);
if data.len() < (allocation_len as usize) {
break;
}
if let Some(last_element_address) = page.last_element_address {
if last_element_address >= start_element_address {
start_element_address = last_element_address + 1;
} else {
bail!("got strange element address");
}
} else {
break;
}
}
if (setup.transport_element_count as usize) != transports.len() {
bail!("got wrong number of transport elements");
}
if (setup.storage_element_count as usize) != storage_slots.len() {
bail!("got wrong number of storage elements");
}
if (setup.import_export_element_count as usize) != import_export_slots.len() {
bail!("got wrong number of import/export elements");
}
if (setup.transfer_element_count as usize) != drives.len() {
bail!("got wrong number of tranfer elements");
}
// create same virtual slot order as mtx(1)
// - storage slots first
// - import export slots at the end
let mut slots = storage_slots;
slots.extend(import_export_slots);
let mut status = MtxStatus { transports, drives, slots };
// sanity checks
if status.drives.is_empty() {
bail!("no data transfer elements reported");
}
if status.slots.is_empty() {
bail!("no storage elements reported");
}
// compute virtual storage slot to element_address map
let mut slot_map = HashMap::new();
for (i, slot) in status.slots.iter().enumerate() {
slot_map.insert(slot.element_address, (i + 1) as u64);
}
// translate element addresses in loaded_lot
for drive in status.drives.iter_mut() {
if let Some(source_address) = drive.loaded_slot {
let source_address = source_address as u16;
drive.loaded_slot = slot_map.get(&source_address).map(|v| *v);
}
}
Ok(status)
}
#[repr(C, packed)]
#[derive(Endian)]
struct ElementStatusHeader {
first_element_address_reported: u16,
number_of_elements_available: u16,
reserved: u8,
byte_count_of_report_available: [u8;3],
}
#[repr(C, packed)]
#[derive(Endian)]
struct SubHeader {
element_type_code: u8,
flags: u8,
descriptor_length: u16,
reseved: u8,
byte_count_of_descriptor_data_available: [u8;3],
}
impl SubHeader {
fn parse_optional_volume_tag<R: Read>(
&self,
reader: &mut R,
full: bool,
) -> Result<Option<String>, Error> {
if (self.flags & 128) != 0 { // has PVolTag
let tmp = reader.read_exact_allocated(36)?;
if full {
let volume_tag = scsi_ascii_to_string(&tmp);
return Ok(Some(volume_tag));
}
}
Ok(None)
}
// AFAIK, tape changer do not use AlternateVolumeTag
// but parse anyways, just to be sure
fn skip_alternate_volume_tag<R: Read>(
&self,
reader: &mut R,
) -> Result<Option<String>, Error> {
if (self.flags & 64) != 0 { // has AVolTag
let _tmp = reader.read_exact_allocated(36)?;
}
Ok(None)
}
}
#[repr(C, packed)]
#[derive(Endian)]
struct TrasnsportDescriptor { // Robot/Griper
element_address: u16,
flags1: u8,
reserved_3: u8,
additional_sense_code: u8,
additional_sense_code_qualifier: u8,
reserved_6: [u8;3],
flags2: u8,
source_storage_element_address: u16,
// volume tag and Mixed media descriptor follows (depends on flags)
}
#[repr(C, packed)]
#[derive(Endian)]
struct TransferDescriptor { // Tape drive
element_address: u16,
flags1: u8,
reserved_3: u8,
additional_sense_code: u8,
additional_sense_code_qualifier: u8,
id_valid: u8,
scsi_bus_address: u8,
reserved_8: u8,
flags2: u8,
source_storage_element_address: u16,
// volume tag, drive identifier and Mixed media descriptor follows
// (depends on flags)
}
#[repr(C, packed)]
#[derive(Endian)]
struct DvcidHead { // Drive Identifier Header
code_set: u8,
identifier_type: u8,
reserved: u8,
identifier_len: u8,
// Identifier follows
}
#[repr(C, packed)]
#[derive(Endian)]
struct StorageDescriptor { // Mail Slot
element_address: u16,
flags1: u8,
reserved_3: u8,
additional_sense_code: u8,
additional_sense_code_qualifier: u8,
reserved_6: [u8;3],
flags2: u8,
source_storage_element_address: u16,
// volume tag and Mixed media descriptor follows (depends on flags)
}
struct DecodedStatusPage {
last_element_address: Option<u16>,
transports: Vec<TransportElementStatus>,
drives: Vec<DriveStatus>,
storage_slots: Vec<StorageElementStatus>,
import_export_slots: Vec<StorageElementStatus>,
}
fn create_element_status(full: bool, volume_tag: Option<String>) -> ElementStatus {
if full {
if let Some(volume_tag) = volume_tag {
ElementStatus::VolumeTag(volume_tag)
} else {
ElementStatus::Full
}
} else {
ElementStatus::Empty
}
}
fn decode_element_status_page(
_info: &InquiryInfo,
data: &[u8],
start_element_address: u16,
) -> Result<DecodedStatusPage, Error> {
proxmox::try_block!({
let mut result = DecodedStatusPage {
last_element_address: None,
transports: Vec::new(),
drives: Vec::new(),
storage_slots: Vec::new(),
import_export_slots: Vec::new(),
};
let mut reader = &data[..];
let head: ElementStatusHeader = unsafe { reader.read_be_value()? };
if head.number_of_elements_available == 0 {
return Ok(result);
}
if head.first_element_address_reported < start_element_address {
bail!("got wrong first_element_address_reported"); // sanity check
}
loop {
if reader.is_empty() {
break;
}
let subhead: SubHeader = unsafe { reader.read_be_value()? };
let len = subhead.byte_count_of_descriptor_data_available;
let mut len = ((len[0] as usize) << 16) + ((len[1] as usize) << 8) + (len[2] as usize);
if len > reader.len() {
len = reader.len();
}
let descr_data = reader.read_exact_allocated(len)?;
let mut reader = &descr_data[..];
loop {
if reader.is_empty() {
break;
}
if reader.len() < (subhead.descriptor_length as usize) {
break;
}
match subhead.element_type_code {
1 => {
let desc: TrasnsportDescriptor = unsafe { reader.read_be_value()? };
let full = (desc.flags1 & 1) != 0;
let volume_tag = subhead.parse_optional_volume_tag(&mut reader, full)?;
subhead.skip_alternate_volume_tag(&mut reader)?;
let mut reserved = [0u8; 4];
reader.read_exact(&mut reserved)?;
result.last_element_address = Some(desc.element_address);
let status = TransportElementStatus {
status: create_element_status(full, volume_tag),
element_address: desc.element_address,
};
result.transports.push(status);
}
2 | 3 => {
let desc: StorageDescriptor = unsafe { reader.read_be_value()? };
let full = (desc.flags1 & 1) != 0;
let volume_tag = subhead.parse_optional_volume_tag(&mut reader, full)?;
subhead.skip_alternate_volume_tag(&mut reader)?;
let mut reserved = [0u8; 4];
reader.read_exact(&mut reserved)?;
result.last_element_address = Some(desc.element_address);
if subhead.element_type_code == 3 {
let status = StorageElementStatus {
import_export: true,
status: create_element_status(full, volume_tag),
element_address: desc.element_address,
};
result.import_export_slots.push(status);
} else {
let status = StorageElementStatus {
import_export: false,
status: create_element_status(full, volume_tag),
element_address: desc.element_address,
};
result.storage_slots.push(status);
}
}
4 => {
let desc: TransferDescriptor = unsafe { reader.read_be_value()? };
let loaded_slot = if (desc.flags2 & 128) != 0 { // SValid
Some(desc.source_storage_element_address as u64)
} else {
None
};
let full = (desc.flags1 & 1) != 0;
let volume_tag = subhead.parse_optional_volume_tag(&mut reader, full)?;
subhead.skip_alternate_volume_tag(&mut reader)?;
let dvcid: DvcidHead = unsafe { reader.read_be_value()? };
let drive_serial_number = match (dvcid.code_set, dvcid.identifier_type) {
(2, 0) => { // Serial number only (Quantum Superloader3 uses this)
let serial = reader.read_exact_allocated(dvcid.identifier_len as usize)?;
let serial = scsi_ascii_to_string(&serial);
Some(serial)
}
(2, 1) => {
if dvcid.identifier_len != 34 {
bail!("got wrong DVCID length");
}
let _vendor = reader.read_exact_allocated(8)?;
let _product = reader.read_exact_allocated(16)?;
let serial = reader.read_exact_allocated(10)?;
let serial = scsi_ascii_to_string(&serial);
Some(serial)
}
_ => None,
};
result.last_element_address = Some(desc.element_address);
let drive = DriveStatus {
loaded_slot,
status: create_element_status(full, volume_tag),
drive_serial_number,
element_address: desc.element_address,
};
result.drives.push(drive);
}
code => bail!("got unknown element type code {}", code),
}
}
}
Ok(result)
}).map_err(|err: Error| format_err!("decode element status failed - {}", err))
}
/// Open the device for read/write, returns the file handle
pub fn open<P: AsRef<Path>>(path: P) -> Result<File, Error> {
let file = OpenOptions::new()
.read(true)
.write(true)
.open(path)?;
Ok(file)
}

View File

@ -20,6 +20,7 @@ use crate::{
MtxStatus, MtxStatus,
DriveStatus, DriveStatus,
ElementStatus, ElementStatus,
StorageElementStatus,
}, },
drive::{ drive::{
VirtualTapeDrive, VirtualTapeDrive,
@ -397,6 +398,8 @@ impl MediaChange for VirtualTapeHandle {
drives.push(DriveStatus { drives.push(DriveStatus {
loaded_slot: None, loaded_slot: None,
status: ElementStatus::VolumeTag(current_tape.name.clone()), status: ElementStatus::VolumeTag(current_tape.name.clone()),
drive_serial_number: None,
element_address: 0,
}); });
} }
@ -408,14 +411,19 @@ impl MediaChange for VirtualTapeHandle {
let max_slots = ((label_texts.len() + 7)/8) * 8; let max_slots = ((label_texts.len() + 7)/8) * 8;
for i in 0..max_slots { for i in 0..max_slots {
if let Some(label_text) = label_texts.get(i) { let status = if let Some(label_text) = label_texts.get(i) {
slots.push((false, ElementStatus::VolumeTag(label_text.clone()))); ElementStatus::VolumeTag(label_text.clone())
} else { } else {
slots.push((false, ElementStatus::Empty)); ElementStatus::Empty
} };
slots.push(StorageElementStatus {
import_export: false,
status,
element_address: (i + 1) as u16,
});
} }
Ok(MtxStatus { drives, slots }) Ok(MtxStatus { drives, slots, transports: Vec::new() })
} }
fn transfer_media(&mut self, _from: u64, _to: u64) -> Result<(), Error> { fn transfer_media(&mut self, _from: u64, _to: u64) -> Result<(), Error> {

View File

@ -91,6 +91,10 @@ const proxmoxOnlineHelpInfo = {
"link": "/docs/sysadmin.html#sysadmin-host-administration", "link": "/docs/sysadmin.html#sysadmin-host-administration",
"title": "Host System Administration" "title": "Host System Administration"
}, },
"restore-encryption-key": {
"link": "/docs/tape-backup.html#restore-encryption-key",
"title": "Restoring Encryption Keys"
},
"user-mgmt": { "user-mgmt": {
"link": "/docs/user-management.html#user-mgmt", "link": "/docs/user-management.html#user-mgmt",
"title": "User Management" "title": "User Management"