tape: add/use rust scsi changer implementation using libsgutil2
This commit is contained in:
parent
a2379996e6
commit
697c41c584
1
TODO.rst
1
TODO.rst
|
@ -36,4 +36,3 @@ Chores:
|
||||||
Suggestions
|
Suggestions
|
||||||
===========
|
===========
|
||||||
|
|
||||||
* tape: rewrite mtx in Rust
|
|
||||||
|
|
|
@ -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?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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::*;
|
|
@ -16,7 +16,9 @@ use crate::{
|
||||||
tape::{
|
tape::{
|
||||||
changer::{
|
changer::{
|
||||||
MtxStatus,
|
MtxStatus,
|
||||||
parse_mtx_status,
|
mtx::{
|
||||||
|
parse_mtx_status,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,36 +4,19 @@ use nom::{
|
||||||
bytes::complete::{take_while, tag},
|
bytes::complete::{take_while, tag},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::tools::nom::{
|
use crate::{
|
||||||
parse_complete, multispace0, multispace1, parse_u64,
|
tools::nom::{
|
||||||
parse_failure, parse_error, IResult,
|
parse_complete, multispace0, multispace1, parse_u64,
|
||||||
|
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))
|
||||||
}
|
}
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ use crate::{
|
||||||
MtxStatus,
|
MtxStatus,
|
||||||
DriveStatus,
|
DriveStatus,
|
||||||
ElementStatus,
|
ElementStatus,
|
||||||
|
StorageElementStatus,
|
||||||
},
|
},
|
||||||
drive::{
|
drive::{
|
||||||
VirtualTapeDrive,
|
VirtualTapeDrive,
|
||||||
|
@ -397,7 +398,9 @@ 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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// This implementation is lame, because we do not have fixed
|
// This implementation is lame, because we do not have fixed
|
||||||
|
@ -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> {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue