tape: add/use rust scsi changer implementation using libsgutil2
This commit is contained in:
parent
a2379996e6
commit
697c41c584
|
@ -22,9 +22,8 @@ use crate::{
|
|||
changer::{
|
||||
OnlineStatusMap,
|
||||
ElementStatus,
|
||||
mtx_status,
|
||||
ScsiMediaChange,
|
||||
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 data: ScsiTapeChanger = config.lookup("changer", &name)?;
|
||||
let mut changer_config: ScsiTapeChanger = config.lookup("changer", &name)?;
|
||||
|
||||
let status = tokio::task::spawn_blocking(move || {
|
||||
mtx_status(&data)
|
||||
changer_config.status()
|
||||
}).await??;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
for (id, (import_export, slot_status)) in status.slots.iter().enumerate() {
|
||||
for (id, slot_info) in status.slots.iter().enumerate() {
|
||||
let entry = MtxStatusEntry {
|
||||
entry_kind: if *import_export {
|
||||
entry_kind: if slot_info.import_export {
|
||||
MtxEntryKind::ImportExport
|
||||
} else {
|
||||
MtxEntryKind::Slot
|
||||
},
|
||||
entry_id: id as u64 + 1,
|
||||
label_text: match &slot_status {
|
||||
label_text: match &slot_info.status {
|
||||
ElementStatus::Empty => None,
|
||||
ElementStatus::Full => Some(String::new()),
|
||||
ElementStatus::VolumeTag(tag) => Some(tag.to_string()),
|
||||
|
@ -129,10 +128,10 @@ pub async fn transfer(
|
|||
|
||||
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 || {
|
||||
mtx_transfer(&data.path, from, to)
|
||||
changer_config.transfer(from, to)
|
||||
}).await?
|
||||
}
|
||||
|
||||
|
|
|
@ -3,20 +3,117 @@
|
|||
mod email;
|
||||
pub use email::*;
|
||||
|
||||
mod parse_mtx_status;
|
||||
pub use parse_mtx_status::*;
|
||||
pub mod sg_pt_changer;
|
||||
|
||||
mod mtx_wrapper;
|
||||
pub use mtx_wrapper::*;
|
||||
|
||||
mod mtx;
|
||||
pub use mtx::*;
|
||||
pub mod mtx;
|
||||
|
||||
mod online_status_map;
|
||||
pub use online_status_map::*;
|
||||
|
||||
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
|
||||
pub trait MediaChange {
|
||||
|
||||
|
@ -79,10 +176,10 @@ pub trait MediaChange {
|
|||
}
|
||||
|
||||
let mut slot = None;
|
||||
for (i, (import_export, element_status)) in status.slots.iter().enumerate() {
|
||||
if let ElementStatus::VolumeTag(tag) = element_status {
|
||||
if *tag == label_text {
|
||||
if *import_export {
|
||||
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);
|
||||
|
@ -117,9 +214,9 @@ pub trait MediaChange {
|
|||
}
|
||||
}
|
||||
|
||||
for (import_export, element_status) in status.slots.iter() {
|
||||
if *import_export { continue; }
|
||||
if let ElementStatus::VolumeTag(ref tag) = element_status {
|
||||
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());
|
||||
}
|
||||
|
@ -137,9 +234,9 @@ pub trait MediaChange {
|
|||
|
||||
let mut cleaning_cartridge_slot = None;
|
||||
|
||||
for (i, (import_export, element_status)) in status.slots.iter().enumerate() {
|
||||
if *import_export { continue; }
|
||||
if let ElementStatus::VolumeTag(ref tag) = element_status {
|
||||
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;
|
||||
|
@ -186,13 +283,13 @@ pub trait MediaChange {
|
|||
let mut from = None;
|
||||
let mut to = None;
|
||||
|
||||
for (i, (import_export, element_status)) in status.slots.iter().enumerate() {
|
||||
if *import_export {
|
||||
for (i, slot_info) in status.slots.iter().enumerate() {
|
||||
if slot_info.import_export {
|
||||
if to.is_some() { continue; }
|
||||
if let ElementStatus::Empty = element_status {
|
||||
if let ElementStatus::Empty = slot_info.status {
|
||||
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 {
|
||||
from = Some(i as u64 + 1);
|
||||
}
|
||||
|
@ -230,7 +327,7 @@ pub trait MediaChange {
|
|||
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 (_import_export, ElementStatus::Empty) = info {
|
||||
if let ElementStatus::Empty = info.status {
|
||||
return self.unload_media(Some(slot));
|
||||
}
|
||||
}
|
||||
|
@ -238,8 +335,8 @@ pub trait MediaChange {
|
|||
|
||||
let mut free_slot = None;
|
||||
for i in 0..status.slots.len() {
|
||||
if status.slots[i].0 { continue; } // skip import/export slots
|
||||
if let ElementStatus::Empty = status.slots[i].1 {
|
||||
if status.slots[i].import_export { continue; } // skip import/export slots
|
||||
if let ElementStatus::Empty = status.slots[i].status {
|
||||
free_slot = Some((i+1) as u64);
|
||||
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,9 +16,11 @@ use crate::{
|
|||
tape::{
|
||||
changer::{
|
||||
MtxStatus,
|
||||
mtx::{
|
||||
parse_mtx_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/// 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() {
|
||||
let slot = i as u64 + 1;
|
||||
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},
|
||||
};
|
||||
|
||||
use crate::tools::nom::{
|
||||
use crate::{
|
||||
tools::nom::{
|
||||
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
|
||||
fn next_line(i: &str) -> IResult<&str, &str> {
|
||||
|
@ -54,12 +37,18 @@ fn parse_storage_changer(i: &str) -> IResult<&str, ()> {
|
|||
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;
|
||||
|
||||
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)?;
|
||||
|
||||
|
@ -78,12 +67,24 @@ fn parse_drive_status(i: &str) -> IResult<&str, DriveStatus> {
|
|||
if let Some(i) = i.strip_prefix(":VolumeTag = ") {
|
||||
let (i, tag) = take_while(|c| !(c == ' ' || c == ':' || c == '\n'))(i)?;
|
||||
let (i, _) = take_while(|c| c != '\n')(i)?; // skip to eol
|
||||
return Ok((i, DriveStatus { loaded_slot, status: ElementStatus::VolumeTag(tag.to_string()) }));
|
||||
let 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
|
||||
|
||||
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> {
|
||||
|
@ -111,7 +112,7 @@ fn parse_data_transfer_element(i: &str) -> IResult<&str, (u64, DriveStatus)> {
|
|||
let (i, _) = multispace1(i)?;
|
||||
let (i, id) = parse_u64(i)?;
|
||||
let (i, _) = nom::character::complete::char(':')(i)?;
|
||||
let (i, element_status) = parse_drive_status(i)?;
|
||||
let (i, element_status) = parse_drive_status(i, id)?;
|
||||
let (i, _) = nom::character::complete::newline(i)?;
|
||||
|
||||
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"));
|
||||
}
|
||||
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))
|
||||
}
|
|
@ -17,7 +17,7 @@ use crate::{
|
|||
MediaChange,
|
||||
MtxStatus,
|
||||
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() {
|
||||
if *import_export { continue; }
|
||||
if let ElementStatus::VolumeTag(ref label_text) = slot_status {
|
||||
for slot_info in status.slots.iter() {
|
||||
if slot_info.import_export { continue; }
|
||||
if let ElementStatus::VolumeTag(ref label_text) = slot_info.status {
|
||||
if let Some(media_id) = inventory.find_media_by_label_text(label_text) {
|
||||
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,
|
||||
DriveStatus,
|
||||
ElementStatus,
|
||||
StorageElementStatus,
|
||||
},
|
||||
drive::{
|
||||
VirtualTapeDrive,
|
||||
|
@ -397,6 +398,8 @@ impl MediaChange for VirtualTapeHandle {
|
|||
drives.push(DriveStatus {
|
||||
loaded_slot: None,
|
||||
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;
|
||||
|
||||
for i in 0..max_slots {
|
||||
if let Some(label_text) = label_texts.get(i) {
|
||||
slots.push((false, ElementStatus::VolumeTag(label_text.clone())));
|
||||
let status = if let Some(label_text) = label_texts.get(i) {
|
||||
ElementStatus::VolumeTag(label_text.clone())
|
||||
} 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> {
|
||||
|
|
|
@ -91,6 +91,10 @@ const proxmoxOnlineHelpInfo = {
|
|||
"link": "/docs/sysadmin.html#sysadmin-host-administration",
|
||||
"title": "Host System Administration"
|
||||
},
|
||||
"restore-encryption-key": {
|
||||
"link": "/docs/tape-backup.html#restore-encryption-key",
|
||||
"title": "Restoring Encryption Keys"
|
||||
},
|
||||
"user-mgmt": {
|
||||
"link": "/docs/user-management.html#user-mgmt",
|
||||
"title": "User Management"
|
||||
|
|
Loading…
Reference in New Issue