diff --git a/TODO.rst b/TODO.rst index 319d448e..3a21ffbd 100644 --- a/TODO.rst +++ b/TODO.rst @@ -36,4 +36,3 @@ Chores: Suggestions =========== -* tape: rewrite mtx in Rust diff --git a/src/api2/tape/changer.rs b/src/api2/tape/changer.rs index eea6f479..da620c4a 100644 --- a/src/api2/tape/changer.rs +++ b/src/api2/tape/changer.rs @@ -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, 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, 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? } diff --git a/src/tape/changer/mod.rs b/src/tape/changer/mod.rs index ec1f1e31..cbcec650 100644 --- a/src/tape/changer/mod.rs +++ b/src/tape/changer/mod.rs @@ -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, + /// The status. + pub status: ElementStatus, + /// Drive Identifier (Serial number) + pub drive_serial_number: Option, + /// 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, + /// List of known storage slots + pub slots: Vec, + /// Tranport elements + /// + /// Note: Some libraries do not report transport elements. + pub transports: Vec, +} + +impl MtxStatus { + + pub fn slot_address(&self, slot: u64) -> Result { + 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 { + 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; + + 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 { + 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 { + 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 { + 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) -> 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) + } + } +} diff --git a/src/tape/changer/mtx.rs b/src/tape/changer/mtx.rs deleted file mode 100644 index 8964217d..00000000 --- a/src/tape/changer/mtx.rs +++ /dev/null @@ -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 { - 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 { - 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) -> 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) - } - } -} diff --git a/src/tape/changer/mtx/mod.rs b/src/tape/changer/mtx/mod.rs new file mode 100644 index 00000000..6ede17df --- /dev/null +++ b/src/tape/changer/mtx/mod.rs @@ -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::*; diff --git a/src/tape/changer/mtx_wrapper.rs b/src/tape/changer/mtx/mtx_wrapper.rs similarity index 94% rename from src/tape/changer/mtx_wrapper.rs rename to src/tape/changer/mtx/mtx_wrapper.rs index 29062422..116382f1 100644 --- a/src/tape/changer/mtx_wrapper.rs +++ b/src/tape/changer/mtx/mtx_wrapper.rs @@ -16,7 +16,9 @@ use crate::{ tape::{ changer::{ MtxStatus, - parse_mtx_status, + mtx::{ + parse_mtx_status, + }, }, }, }; @@ -48,7 +50,7 @@ pub fn mtx_status(config: &ScsiTapeChanger) -> Result { 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 } } diff --git a/src/tape/changer/parse_mtx_status.rs b/src/tape/changer/mtx/parse_mtx_status.rs similarity index 73% rename from src/tape/changer/parse_mtx_status.rs rename to src/tape/changer/mtx/parse_mtx_status.rs index fdc0bf42..cdfcba76 100644 --- a/src/tape/changer/parse_mtx_status.rs +++ b/src/tape/changer/mtx/parse_mtx_status.rs @@ -4,36 +4,19 @@ use nom::{ bytes::complete::{take_while, tag}, }; -use crate::tools::nom::{ - parse_complete, multispace0, multispace1, parse_u64, - parse_failure, parse_error, IResult, +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, - /// The status. - pub status: ElementStatus, -} - -/// Changer status - show drive/slot usage -pub struct MtxStatus { - /// List of known drives - pub drives: Vec, - /// 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)) } diff --git a/src/tape/changer/online_status_map.rs b/src/tape/changer/online_status_map.rs index 53f5fa45..869df5dc 100644 --- a/src/tape/changer/online_status_map.rs +++ b/src/tape/changer/online_status_map.rs @@ -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()); } diff --git a/src/tape/changer/sg_pt_changer.rs b/src/tape/changer/sg_pt_changer.rs new file mode 100644 index 00000000..4e342181 --- /dev/null +++ b/src/tape/changer/sg_pt_changer.rs @@ -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(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( + file: &mut F, +) -> Result { + + 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 { + + 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( + 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 { + + 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(file: &mut F) -> Result { + + 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( + &self, + reader: &mut R, + full: bool, + ) -> Result, 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( + &self, + reader: &mut R, + ) -> Result, 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, + transports: Vec, + drives: Vec, + storage_slots: Vec, + import_export_slots: Vec, +} + +fn create_element_status(full: bool, volume_tag: Option) -> 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 { + + 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>(path: P) -> Result { + let file = OpenOptions::new() + .read(true) + .write(true) + .open(path)?; + + Ok(file) +} diff --git a/src/tape/drive/virtual_tape.rs b/src/tape/drive/virtual_tape.rs index 218c4f83..7dea49de 100644 --- a/src/tape/drive/virtual_tape.rs +++ b/src/tape/drive/virtual_tape.rs @@ -20,6 +20,7 @@ use crate::{ MtxStatus, DriveStatus, ElementStatus, + StorageElementStatus, }, drive::{ VirtualTapeDrive, @@ -397,7 +398,9 @@ impl MediaChange for VirtualTapeHandle { drives.push(DriveStatus { loaded_slot: None, status: ElementStatus::VolumeTag(current_tape.name.clone()), - }); + drive_serial_number: None, + element_address: 0, + }); } // 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; 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> { diff --git a/www/OnlineHelpInfo.js b/www/OnlineHelpInfo.js index c54912d8..ece5a012 100644 --- a/www/OnlineHelpInfo.js +++ b/www/OnlineHelpInfo.js @@ -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"