tape: implement LTO userspace driver
This commit is contained in:
@ -26,7 +26,7 @@ use proxmox::{
|
||||
use crate::api2::types::{
|
||||
SLOT_ARRAY_SCHEMA,
|
||||
ScsiTapeChanger,
|
||||
LinuxTapeDrive,
|
||||
LtoTapeDrive,
|
||||
};
|
||||
|
||||
/// Changer element status.
|
||||
@ -523,7 +523,7 @@ pub struct MtxMediaChanger {
|
||||
|
||||
impl MtxMediaChanger {
|
||||
|
||||
pub fn with_drive_config(drive_config: &LinuxTapeDrive) -> Result<Self, Error> {
|
||||
pub fn with_drive_config(drive_config: &LtoTapeDrive) -> 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)?,
|
||||
|
420
src/tape/drive/lto/mod.rs
Normal file
420
src/tape/drive/lto/mod.rs
Normal file
@ -0,0 +1,420 @@
|
||||
//! Driver for LTO SCSI tapes
|
||||
//!
|
||||
//! This is a userspace drive implementation using SG_IO.
|
||||
//!
|
||||
//! Why we do not use the Linux tape driver:
|
||||
//!
|
||||
//! - missing features (MAM, Encryption, ...)
|
||||
//!
|
||||
//! - strange permission handling - only root (or CAP_SYS_RAWIO) can
|
||||
//! do SG_IO (SYS_RAW_IO)
|
||||
//!
|
||||
//! - unability to detect EOT (you just get EIO)
|
||||
|
||||
mod sg_tape;
|
||||
pub use sg_tape::*;
|
||||
|
||||
use std::fs::{OpenOptions, File};
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use nix::fcntl::{fcntl, FcntlArg, OFlag};
|
||||
|
||||
use proxmox::{
|
||||
tools::Uuid,
|
||||
sys::error::SysResult,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config,
|
||||
tools::run_command,
|
||||
backup::{
|
||||
Fingerprint,
|
||||
KeyConfig,
|
||||
},
|
||||
api2::types::{
|
||||
MamAttribute,
|
||||
LtoDriveAndMediaStatus,
|
||||
LtoTapeDrive,
|
||||
},
|
||||
tape::{
|
||||
TapeRead,
|
||||
TapeWrite,
|
||||
drive::{
|
||||
TapeDriver,
|
||||
TapeAlertFlags,
|
||||
Lp17VolumeStatistics,
|
||||
mam_extract_media_usage,
|
||||
},
|
||||
file_formats::{
|
||||
PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0,
|
||||
MediaSetLabel,
|
||||
MediaContentHeader,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
impl LtoTapeDrive {
|
||||
|
||||
/// Open a tape device
|
||||
///
|
||||
/// This does additional checks:
|
||||
///
|
||||
/// - check if it is a non-rewinding tape device
|
||||
/// - check if drive is ready (tape loaded)
|
||||
/// - check block size
|
||||
/// - for autoloader only, try to reload ejected tapes
|
||||
pub fn open(&self) -> Result<LtoTapeHandle, Error> {
|
||||
|
||||
proxmox::try_block!({
|
||||
let file = open_lto_tape_device(&self.path)?;
|
||||
|
||||
let mut handle = LtoTapeHandle::new(file)?;
|
||||
|
||||
if !handle.sg_tape.test_unit_ready().is_ok() {
|
||||
// for autoloader only, try to reload ejected tapes
|
||||
if self.changer.is_some() {
|
||||
let _ = handle.sg_tape.load(); // just try, ignore error
|
||||
}
|
||||
}
|
||||
|
||||
handle.sg_tape.wait_until_ready()?;
|
||||
|
||||
// Only root can set driver options, so we cannot
|
||||
// handle.set_default_options()?;
|
||||
|
||||
Ok(handle)
|
||||
}).map_err(|err: Error| format_err!("open drive '{}' ({}) failed - {}", self.name, self.path, err))
|
||||
}
|
||||
}
|
||||
|
||||
/// Lto Tape device handle
|
||||
pub struct LtoTapeHandle {
|
||||
sg_tape: SgTape,
|
||||
}
|
||||
|
||||
impl LtoTapeHandle {
|
||||
|
||||
/// Creates a new instance
|
||||
pub fn new(file: File) -> Result<Self, Error> {
|
||||
let sg_tape = SgTape::new(file)?;
|
||||
Ok(Self { sg_tape })
|
||||
}
|
||||
|
||||
/// Set all options we need/want
|
||||
pub fn set_default_options(&self) -> Result<(), Error> {
|
||||
// fixme
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write a single EOF mark without flushing buffers
|
||||
pub fn write_filemarks(&mut self, count: usize) -> Result<(), std::io::Error> {
|
||||
self.sg_tape.write_filemarks(count, false)
|
||||
}
|
||||
|
||||
/// Get Tape and Media status
|
||||
pub fn get_drive_and_media_status(&mut self) -> Result<LtoDriveAndMediaStatus, Error> {
|
||||
|
||||
let (file_number, block_number) = match self.sg_tape.position() {
|
||||
Ok(position) => (
|
||||
Some(position.logical_file_id),
|
||||
Some(position.logical_object_number),
|
||||
),
|
||||
Err(_) => (None, None),
|
||||
};
|
||||
|
||||
let options = String::from("FIXME");
|
||||
|
||||
let alert_flags = self.tape_alert_flags()
|
||||
.map(|flags| format!("{:?}", flags))
|
||||
.ok();
|
||||
|
||||
let mut status = LtoDriveAndMediaStatus {
|
||||
blocksize: 0, // fixme: remove
|
||||
density: None, // fixme
|
||||
status: String::from("FIXME"),
|
||||
options,
|
||||
alert_flags,
|
||||
file_number,
|
||||
block_number,
|
||||
manufactured: None,
|
||||
bytes_read: None,
|
||||
bytes_written: None,
|
||||
medium_passes: None,
|
||||
medium_wearout: None,
|
||||
volume_mounts: None,
|
||||
};
|
||||
|
||||
if self.sg_tape.test_unit_ready()? {
|
||||
|
||||
if let Ok(mam) = self.cartridge_memory() {
|
||||
|
||||
let usage = mam_extract_media_usage(&mam)?;
|
||||
|
||||
status.manufactured = Some(usage.manufactured);
|
||||
status.bytes_read = Some(usage.bytes_read);
|
||||
status.bytes_written = Some(usage.bytes_written);
|
||||
|
||||
if let Ok(volume_stats) = self.volume_statistics() {
|
||||
|
||||
let passes = std::cmp::max(
|
||||
volume_stats.beginning_of_medium_passes,
|
||||
volume_stats.middle_of_tape_passes,
|
||||
);
|
||||
|
||||
// assume max. 16000 medium passes
|
||||
// see: https://en.wikipedia.org/wiki/Linear_Tape-Open
|
||||
let wearout: f64 = (passes as f64)/(16000.0 as f64);
|
||||
|
||||
status.medium_passes = Some(passes);
|
||||
status.medium_wearout = Some(wearout);
|
||||
|
||||
status.volume_mounts = Some(volume_stats.volume_mounts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
pub fn load(&mut self) -> Result<(), Error> {
|
||||
self.sg_tape.load()
|
||||
}
|
||||
|
||||
/// Read Cartridge Memory (MAM Attributes)
|
||||
pub fn cartridge_memory(&mut self) -> Result<Vec<MamAttribute>, Error> {
|
||||
self.sg_tape.cartridge_memory()
|
||||
}
|
||||
|
||||
/// Read Volume Statistics
|
||||
pub fn volume_statistics(&mut self) -> Result<Lp17VolumeStatistics, Error> {
|
||||
self.sg_tape.volume_statistics()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl TapeDriver for LtoTapeHandle {
|
||||
|
||||
fn sync(&mut self) -> Result<(), Error> {
|
||||
self.sg_tape.sync()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Go to the end of the recorded media (for appending files).
|
||||
fn move_to_eom(&mut self) -> Result<(), Error> {
|
||||
self.sg_tape.move_to_eom()
|
||||
}
|
||||
|
||||
fn forward_space_count_files(&mut self, count: usize) -> Result<(), Error> {
|
||||
self.sg_tape.space_filemarks(isize::try_from(count)?)
|
||||
}
|
||||
|
||||
fn backward_space_count_files(&mut self, count: usize) -> Result<(), Error> {
|
||||
self.sg_tape.space_filemarks(-isize::try_from(count)?)
|
||||
}
|
||||
|
||||
fn rewind(&mut self) -> Result<(), Error> {
|
||||
self.sg_tape.rewind()
|
||||
}
|
||||
|
||||
fn current_file_number(&mut self) -> Result<u64, Error> {
|
||||
self.sg_tape.current_file_number()
|
||||
}
|
||||
|
||||
fn erase_media(&mut self, fast: bool) -> Result<(), Error> {
|
||||
self.rewind()?; // important - erase from BOT
|
||||
self.sg_tape.erase_media(fast)
|
||||
}
|
||||
|
||||
fn read_next_file<'a>(&'a mut self) -> Result<Option<Box<dyn TapeRead + 'a>>, std::io::Error> {
|
||||
let reader = self.sg_tape.open_reader()?;
|
||||
let handle = match reader {
|
||||
Some(reader) => {
|
||||
let reader: Box<dyn TapeRead> = Box::new(reader);
|
||||
Some(reader)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
fn write_file<'a>(&'a mut self) -> Result<Box<dyn TapeWrite + 'a>, std::io::Error> {
|
||||
let handle = self.sg_tape.open_writer();
|
||||
Ok(Box::new(handle))
|
||||
}
|
||||
|
||||
fn write_media_set_label(
|
||||
&mut self,
|
||||
media_set_label: &MediaSetLabel,
|
||||
key_config: Option<&KeyConfig>,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let file_number = self.current_file_number()?;
|
||||
if file_number != 1 {
|
||||
self.rewind()?;
|
||||
self.forward_space_count_files(1)?; // skip label
|
||||
}
|
||||
|
||||
let file_number = self.current_file_number()?;
|
||||
if file_number != 1 {
|
||||
bail!("write_media_set_label failed - got wrong file number ({} != 1)", file_number);
|
||||
}
|
||||
|
||||
self.set_encryption(None)?;
|
||||
|
||||
{ // limit handle scope
|
||||
let mut handle = self.write_file()?;
|
||||
|
||||
let mut value = serde_json::to_value(media_set_label)?;
|
||||
if media_set_label.encryption_key_fingerprint.is_some() {
|
||||
match key_config {
|
||||
Some(key_config) => {
|
||||
value["key-config"] = serde_json::to_value(key_config)?;
|
||||
}
|
||||
None => {
|
||||
bail!("missing encryption key config");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let raw = serde_json::to_string_pretty(&value)?;
|
||||
|
||||
let header = MediaContentHeader::new(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, raw.len() as u32);
|
||||
handle.write_header(&header, raw.as_bytes())?;
|
||||
handle.finish(false)?;
|
||||
}
|
||||
|
||||
self.sync()?; // sync data to tape
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rewind and put the drive off line (Eject media).
|
||||
fn eject_media(&mut self) -> Result<(), Error> {
|
||||
self.sg_tape.eject()
|
||||
}
|
||||
|
||||
/// Read Tape Alert Flags
|
||||
fn tape_alert_flags(&mut self) -> Result<TapeAlertFlags, Error> {
|
||||
self.sg_tape.tape_alert_flags()
|
||||
}
|
||||
|
||||
/// Set or clear encryption key
|
||||
///
|
||||
/// Note: Only 'root' can read secret encryption keys, so we need
|
||||
/// to spawn setuid binary 'sg-tape-cmd'.
|
||||
fn set_encryption(
|
||||
&mut self,
|
||||
key_fingerprint: Option<(Fingerprint, Uuid)>,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
if nix::unistd::Uid::effective().is_root() {
|
||||
|
||||
if let Some((ref key_fingerprint, ref uuid)) = key_fingerprint {
|
||||
|
||||
let (key_map, _digest) = config::tape_encryption_keys::load_keys()?;
|
||||
match key_map.get(key_fingerprint) {
|
||||
Some(item) => {
|
||||
|
||||
// derive specialized key for each media-set
|
||||
|
||||
let mut tape_key = [0u8; 32];
|
||||
|
||||
let uuid_bytes: [u8; 16] = uuid.as_bytes().clone();
|
||||
|
||||
openssl::pkcs5::pbkdf2_hmac(
|
||||
&item.key,
|
||||
&uuid_bytes,
|
||||
10,
|
||||
openssl::hash::MessageDigest::sha256(),
|
||||
&mut tape_key)?;
|
||||
|
||||
return self.sg_tape.set_encryption(Some(tape_key));
|
||||
}
|
||||
None => bail!("unknown tape encryption key '{}'", key_fingerprint),
|
||||
}
|
||||
} else {
|
||||
return self.sg_tape.set_encryption(None);
|
||||
}
|
||||
}
|
||||
|
||||
let output = if let Some((fingerprint, uuid)) = key_fingerprint {
|
||||
let fingerprint = crate::tools::format::as_fingerprint(fingerprint.bytes());
|
||||
run_sg_tape_cmd("encryption", &[
|
||||
"--fingerprint", &fingerprint,
|
||||
"--uuid", &uuid.to_string(),
|
||||
], self.sg_tape.file_mut().as_raw_fd())?
|
||||
} else {
|
||||
run_sg_tape_cmd("encryption", &[], self.sg_tape.file_mut().as_raw_fd())?
|
||||
};
|
||||
let result: Result<(), String> = serde_json::from_str(&output)?;
|
||||
result.map_err(|err| format_err!("{}", err))
|
||||
}
|
||||
}
|
||||
|
||||
/// Check for correct Major/Minor numbers
|
||||
pub fn check_tape_is_lto_tape_device(file: &File) -> Result<(), Error> {
|
||||
|
||||
let stat = nix::sys::stat::fstat(file.as_raw_fd())?;
|
||||
|
||||
let devnum = stat.st_rdev;
|
||||
|
||||
let major = unsafe { libc::major(devnum) };
|
||||
let _minor = unsafe { libc::minor(devnum) };
|
||||
|
||||
if major == 9 {
|
||||
bail!("not a scsi-generic tape device (cannot use linux tape devices)");
|
||||
}
|
||||
|
||||
if major != 21 {
|
||||
bail!("not a scsi-generic tape device");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Opens a Lto tape device
|
||||
///
|
||||
/// The open call use O_NONBLOCK, but that flag is cleard after open
|
||||
/// succeeded. This also checks if the device is a non-rewinding tape
|
||||
/// device.
|
||||
pub fn open_lto_tape_device(
|
||||
path: &str,
|
||||
) -> Result<File, Error> {
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.custom_flags(libc::O_NONBLOCK)
|
||||
.open(path)?;
|
||||
|
||||
// clear O_NONBLOCK from now on.
|
||||
|
||||
let flags = fcntl(file.as_raw_fd(), FcntlArg::F_GETFL)
|
||||
.into_io_result()?;
|
||||
|
||||
let mut flags = OFlag::from_bits_truncate(flags);
|
||||
flags.remove(OFlag::O_NONBLOCK);
|
||||
|
||||
fcntl(file.as_raw_fd(), FcntlArg::F_SETFL(flags))
|
||||
.into_io_result()?;
|
||||
|
||||
check_tape_is_lto_tape_device(&file)
|
||||
.map_err(|err| format_err!("device type check {:?} failed - {}", path, err))?;
|
||||
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
fn run_sg_tape_cmd(subcmd: &str, args: &[&str], fd: RawFd) -> Result<String, Error> {
|
||||
let mut command = std::process::Command::new(
|
||||
"/usr/lib/x86_64-linux-gnu/proxmox-backup/sg-tape-cmd");
|
||||
command.args(&[subcmd]);
|
||||
command.args(&["--stdin"]);
|
||||
command.args(args);
|
||||
let device_fd = nix::unistd::dup(fd)?;
|
||||
command.stdin(unsafe { std::process::Stdio::from_raw_fd(device_fd)});
|
||||
run_command(command, None)
|
||||
}
|
445
src/tape/drive/lto/sg_tape.rs
Normal file
445
src/tape/drive/lto/sg_tape.rs
Normal file
@ -0,0 +1,445 @@
|
||||
use std::time::SystemTime;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use endian_trait::Endian;
|
||||
use nix::fcntl::{fcntl, FcntlArg, OFlag};
|
||||
|
||||
use proxmox::{
|
||||
sys::error::SysResult,
|
||||
tools::io::ReadExt,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
api2::types::{
|
||||
MamAttribute,
|
||||
},
|
||||
tape::{
|
||||
BlockRead,
|
||||
BlockReadStatus,
|
||||
BlockWrite,
|
||||
file_formats::{
|
||||
BlockedWriter,
|
||||
BlockedReader,
|
||||
},
|
||||
drive::{
|
||||
TapeAlertFlags,
|
||||
Lp17VolumeStatistics,
|
||||
read_mam_attributes,
|
||||
read_tape_alert_flags,
|
||||
read_volume_statistics,
|
||||
set_encryption,
|
||||
},
|
||||
},
|
||||
tools::sgutils2::{
|
||||
SgRaw,
|
||||
SenseInfo,
|
||||
ScsiError,
|
||||
InquiryInfo,
|
||||
scsi_inquiry,
|
||||
},
|
||||
};
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Endian, Debug, Copy, Clone)]
|
||||
pub struct ReadPositionLongPage {
|
||||
flags: u8,
|
||||
reserved: [u8;3],
|
||||
partition_number: u32,
|
||||
pub logical_object_number: u64,
|
||||
pub logical_file_id: u64,
|
||||
obsolete: [u8;8],
|
||||
}
|
||||
|
||||
pub struct SgTape {
|
||||
file: File,
|
||||
}
|
||||
|
||||
impl SgTape {
|
||||
|
||||
const SCSI_TAPE_DEFAULT_TIMEOUT: usize = 60*2; // 2 minutes
|
||||
|
||||
/// Create a new instance
|
||||
///
|
||||
/// Uses scsi_inquiry to check the device type.
|
||||
pub fn new(mut file: File) -> Result<Self, Error> {
|
||||
|
||||
let info = scsi_inquiry(&mut file)?;
|
||||
|
||||
if info.peripheral_type != 1 {
|
||||
bail!("not a tape device (peripheral_type = {})", info.peripheral_type);
|
||||
}
|
||||
Ok(Self { file })
|
||||
}
|
||||
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> Result<SgTape, Error> {
|
||||
// do not wait for media, use O_NONBLOCK
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.custom_flags(libc::O_NONBLOCK)
|
||||
.open(path)?;
|
||||
|
||||
// then clear O_NONBLOCK
|
||||
let flags = fcntl(file.as_raw_fd(), FcntlArg::F_GETFL)
|
||||
.into_io_result()?;
|
||||
|
||||
let mut flags = OFlag::from_bits_truncate(flags);
|
||||
flags.remove(OFlag::O_NONBLOCK);
|
||||
|
||||
fcntl(file.as_raw_fd(), FcntlArg::F_SETFL(flags))
|
||||
.into_io_result()?;
|
||||
|
||||
Self::new(file)
|
||||
}
|
||||
|
||||
pub fn inquiry(&mut self) -> Result<InquiryInfo, Error> {
|
||||
scsi_inquiry(&mut self.file)
|
||||
}
|
||||
|
||||
pub fn erase_media(&mut self, _fast: bool) -> Result<(), Error> {
|
||||
// fixme:
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
pub fn rewind(&mut self) -> Result<(), Error> {
|
||||
|
||||
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
|
||||
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
|
||||
let mut cmd = Vec::new();
|
||||
cmd.extend(&[0x01, 0, 0, 0, 0, 0]); // REWIND
|
||||
|
||||
sg_raw.do_command(&cmd)
|
||||
.map_err(|err| format_err!("rewind failed - {}", err))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn position(&mut self) -> Result<ReadPositionLongPage, Error> {
|
||||
|
||||
let expected_size = std::mem::size_of::<ReadPositionLongPage>();
|
||||
|
||||
let mut sg_raw = SgRaw::new(&mut self.file, 32)?;
|
||||
sg_raw.set_timeout(30); // use short timeout
|
||||
let mut cmd = Vec::new();
|
||||
cmd.extend(&[0x34, 0x06, 0, 0, 0, 0, 0, 0, 0, 0]); // READ POSITION LONG FORM
|
||||
|
||||
let data = sg_raw.do_command(&cmd)
|
||||
.map_err(|err| format_err!("read position failed - {}", err))?;
|
||||
|
||||
let page = proxmox::try_block!({
|
||||
if data.len() != expected_size {
|
||||
bail!("got unexpected data len ({} != {}", data.len(), expected_size);
|
||||
}
|
||||
|
||||
let mut reader = &data[..];
|
||||
|
||||
let page: ReadPositionLongPage = unsafe { reader.read_be_value()? };
|
||||
|
||||
Ok(page)
|
||||
}).map_err(|err: Error| format_err!("decode position page failed - {}", err))?;
|
||||
|
||||
if page.partition_number != 0 {
|
||||
bail!("detecthed partitioned tape - not supported");
|
||||
}
|
||||
|
||||
println!("DATA: {:?}", page);
|
||||
|
||||
Ok(page)
|
||||
}
|
||||
|
||||
pub fn current_file_number(&mut self) -> Result<u64, Error> {
|
||||
let position = self.position()?;
|
||||
Ok(position.logical_file_id)
|
||||
}
|
||||
|
||||
pub fn locate(&mut self) -> Result<(), Error> {
|
||||
// fixme: impl LOCATE
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
pub fn move_to_eom(&mut self) -> Result<(), Error> {
|
||||
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
|
||||
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
|
||||
let mut cmd = Vec::new();
|
||||
cmd.extend(&[0x11, 0x03, 0, 0, 0, 0]); // SPACE(6) move to EOD
|
||||
|
||||
sg_raw.do_command(&cmd)
|
||||
.map_err(|err| format_err!("move to EOD failed - {}", err))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn space_filemarks(&mut self, count: isize) -> Result<(), Error> {
|
||||
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
|
||||
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
|
||||
let mut cmd = Vec::new();
|
||||
|
||||
// Use short command if possible (supported by all drives)
|
||||
if (count <= 0x7fffff) && (count > -0x7fffff) {
|
||||
cmd.extend(&[0x11, 0x01]); // SPACE(6) with filemarks
|
||||
cmd.push(((count >> 16) & 0xff) as u8);
|
||||
cmd.push(((count >> 8) & 0xff) as u8);
|
||||
cmd.push((count & 0xff) as u8);
|
||||
cmd.push(0); //control byte
|
||||
} else {
|
||||
|
||||
cmd.extend(&[0x91, 0x01, 0, 0]); // SPACE(16) with filemarks
|
||||
let count: i64 = count as i64;
|
||||
cmd.extend(&count.to_be_bytes());
|
||||
cmd.extend(&[0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
sg_raw.do_command(&cmd)
|
||||
.map_err(|err| format_err!("space filemarks failed - {}", err))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn eject(&mut self) -> Result<(), Error> {
|
||||
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
|
||||
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
|
||||
let mut cmd = Vec::new();
|
||||
cmd.extend(&[0x1B, 0, 0, 0, 0, 0]); // LODA/UNLOAD HOLD=0, LOAD=0
|
||||
|
||||
sg_raw.do_command(&cmd)
|
||||
.map_err(|err| format_err!("eject failed - {}", err))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load(&mut self) -> Result<(), Error> {
|
||||
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
|
||||
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
|
||||
let mut cmd = Vec::new();
|
||||
cmd.extend(&[0x1B, 0, 0, 0, 0b0000_0001, 0]); // LODA/UNLOAD HOLD=0, LOAD=1
|
||||
|
||||
sg_raw.do_command(&cmd)
|
||||
.map_err(|err| format_err!("load media failed - {}", err))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_filemarks(
|
||||
&mut self,
|
||||
count: usize,
|
||||
immediate: bool,
|
||||
) -> Result<(), std::io::Error> {
|
||||
|
||||
if count > 255 {
|
||||
proxmox::io_bail!("write_filemarks failed: got strange count '{}'", count);
|
||||
}
|
||||
|
||||
let mut sg_raw = SgRaw::new(&mut self.file, 16)
|
||||
.map_err(|err| proxmox::io_format_err!("write_filemarks failed (alloc) - {}", err))?;
|
||||
|
||||
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
|
||||
let mut cmd = Vec::new();
|
||||
cmd.push(0x10);
|
||||
if immediate {
|
||||
cmd.push(1); // IMMED=1
|
||||
} else {
|
||||
cmd.push(0); // IMMED=0
|
||||
}
|
||||
cmd.extend(&[0, 0, count as u8]); // COUNT
|
||||
cmd.push(0); // control byte
|
||||
|
||||
sg_raw.do_command(&cmd)
|
||||
.map_err(|err| proxmox::io_format_err!("write filemark failed - {}", err))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Flush tape buffers (WEOF with count 0 => flush)
|
||||
pub fn sync(&mut self) -> Result<(), std::io::Error> {
|
||||
self.write_filemarks(0, false)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn test_unit_ready(&mut self) -> Result<bool, Error> {
|
||||
|
||||
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
|
||||
sg_raw.set_timeout(30); // use short timeout
|
||||
let mut cmd = Vec::new();
|
||||
cmd.extend(&[0x00, 0, 0, 0, 0, 0]); // TEST UNIT READY
|
||||
|
||||
// fixme: check sense
|
||||
sg_raw.do_command(&cmd)
|
||||
.map_err(|err| format_err!("unit not ready - {}", err))?;
|
||||
|
||||
Ok(true)
|
||||
|
||||
}
|
||||
|
||||
pub fn wait_until_ready(&mut self) -> Result<(), Error> {
|
||||
|
||||
let start = SystemTime::now();
|
||||
let max_wait = std::time::Duration::new(Self::SCSI_TAPE_DEFAULT_TIMEOUT as u64, 0);
|
||||
|
||||
loop {
|
||||
match self.test_unit_ready() {
|
||||
Ok(true) => return Ok(()),
|
||||
_ => {
|
||||
std::thread::sleep(std::time::Duration::new(1, 0));
|
||||
if start.elapsed()? > max_wait {
|
||||
bail!("wait_until_ready failed - got timeout");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read Tape Alert Flags
|
||||
pub fn tape_alert_flags(&mut self) -> Result<TapeAlertFlags, Error> {
|
||||
read_tape_alert_flags(&mut self.file)
|
||||
}
|
||||
|
||||
/// Read Cartridge Memory (MAM Attributes)
|
||||
pub fn cartridge_memory(&mut self) -> Result<Vec<MamAttribute>, Error> {
|
||||
read_mam_attributes(&mut self.file)
|
||||
}
|
||||
|
||||
/// Read Volume Statistics
|
||||
pub fn volume_statistics(&mut self) -> Result<Lp17VolumeStatistics, Error> {
|
||||
return read_volume_statistics(&mut self.file);
|
||||
}
|
||||
|
||||
pub fn set_encryption(
|
||||
&mut self,
|
||||
key: Option<[u8; 32]>,
|
||||
) -> Result<(), Error> {
|
||||
set_encryption(&mut self.file, key)
|
||||
}
|
||||
|
||||
// Note: use alloc_page_aligned_buffer to alloc data transfer buffer
|
||||
//
|
||||
// Returns true if the drive reached the Logical End Of Media (early warning)
|
||||
fn write_block(&mut self, data: &[u8]) -> Result<bool, std::io::Error> {
|
||||
|
||||
let transfer_len = data.len();
|
||||
|
||||
if transfer_len > 0xFFFFFF {
|
||||
proxmox::io_bail!("write failed - data too large");
|
||||
}
|
||||
|
||||
let mut sg_raw = SgRaw::new(&mut self.file, 0)
|
||||
.unwrap(); // cannot fail with size 0
|
||||
|
||||
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
|
||||
let mut cmd = Vec::new();
|
||||
cmd.push(0x0A); // WRITE
|
||||
cmd.push(0x00); // VARIABLE SIZED BLOCKS
|
||||
cmd.push(((transfer_len >> 16) & 0xff) as u8);
|
||||
cmd.push(((transfer_len >> 8) & 0xff) as u8);
|
||||
cmd.push((transfer_len & 0xff) as u8);
|
||||
cmd.push(0); // control byte
|
||||
|
||||
//println!("WRITE {:?}", cmd);
|
||||
//println!("WRITE {:?}", data);
|
||||
|
||||
sg_raw.do_out_command(&cmd, data)
|
||||
.map_err(|err| proxmox::io_format_err!("write failed - {}", err))?;
|
||||
|
||||
// fixme: LEOM?
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn read_block(&mut self, buffer: &mut [u8]) -> Result<BlockReadStatus, std::io::Error> {
|
||||
let transfer_len = buffer.len();
|
||||
|
||||
if transfer_len > 0xFFFFFF {
|
||||
proxmox::io_bail!("read failed - buffer too large");
|
||||
}
|
||||
|
||||
let mut sg_raw = SgRaw::new(&mut self.file, 0)
|
||||
.unwrap(); // cannot fail with size 0
|
||||
|
||||
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
|
||||
let mut cmd = Vec::new();
|
||||
cmd.push(0x08); // READ
|
||||
cmd.push(0x02); // VARIABLE SIZED BLOCKS, SILI=1
|
||||
//cmd.push(0x00); // VARIABLE SIZED BLOCKS, SILI=0
|
||||
cmd.push(((transfer_len >> 16) & 0xff) as u8);
|
||||
cmd.push(((transfer_len >> 8) & 0xff) as u8);
|
||||
cmd.push((transfer_len & 0xff) as u8);
|
||||
cmd.push(0); // control byte
|
||||
|
||||
let data = match sg_raw.do_in_command(&cmd, buffer) {
|
||||
Ok(data) => data,
|
||||
Err(ScsiError::Sense(SenseInfo { sense_key: 0, asc: 0, ascq: 1 })) => {
|
||||
return Ok(BlockReadStatus::EndOfFile);
|
||||
}
|
||||
Err(ScsiError::Sense(SenseInfo { sense_key: 8, asc: 0, ascq: 5 })) => {
|
||||
return Ok(BlockReadStatus::EndOfStream);
|
||||
}
|
||||
Err(err) => {
|
||||
println!("READ ERR {:?}", err);
|
||||
proxmox::io_bail!("read failed - {}", err);
|
||||
}
|
||||
};
|
||||
|
||||
if data.len() != transfer_len {
|
||||
proxmox::io_bail!("read failed - unexpected block len ({} != {})", data.len(), buffer.len())
|
||||
}
|
||||
|
||||
Ok(BlockReadStatus::Ok(transfer_len))
|
||||
}
|
||||
|
||||
pub fn open_writer(&mut self) -> BlockedWriter<SgTapeWriter> {
|
||||
let writer = SgTapeWriter::new(self);
|
||||
BlockedWriter::new(writer)
|
||||
}
|
||||
|
||||
pub fn open_reader(&mut self) -> Result<Option<BlockedReader<SgTapeReader>>, std::io::Error> {
|
||||
let reader = SgTapeReader::new(self);
|
||||
match BlockedReader::open(reader)? {
|
||||
Some(reader) => Ok(Some(reader)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SgTapeReader<'a> {
|
||||
sg_tape: &'a mut SgTape,
|
||||
}
|
||||
|
||||
impl <'a> SgTapeReader<'a> {
|
||||
|
||||
pub fn new(sg_tape: &'a mut SgTape) -> Self {
|
||||
Self { sg_tape }
|
||||
}
|
||||
}
|
||||
|
||||
impl <'a> BlockRead for SgTapeReader<'a> {
|
||||
|
||||
fn read_block(&mut self, buffer: &mut [u8]) -> Result<BlockReadStatus, std::io::Error> {
|
||||
self.sg_tape.read_block(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SgTapeWriter<'a> {
|
||||
sg_tape: &'a mut SgTape,
|
||||
_leom_sent: bool,
|
||||
}
|
||||
|
||||
impl <'a> SgTapeWriter<'a> {
|
||||
|
||||
pub fn new(sg_tape: &'a mut SgTape) -> Self {
|
||||
Self { sg_tape, _leom_sent: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl <'a> BlockWrite for SgTapeWriter<'a> {
|
||||
|
||||
fn write_block(&mut self, buffer: &[u8]) -> Result<bool, std::io::Error> {
|
||||
self.sg_tape.write_block(buffer)
|
||||
}
|
||||
|
||||
fn write_filemark(&mut self) -> Result<(), std::io::Error> {
|
||||
self.sg_tape.write_filemarks(1, true)
|
||||
}
|
||||
}
|
@ -13,8 +13,8 @@ pub use volume_statistics::*;
|
||||
mod encryption;
|
||||
pub use encryption::*;
|
||||
|
||||
mod linux_tape;
|
||||
pub use linux_tape::*;
|
||||
mod lto;
|
||||
pub use lto::*;
|
||||
|
||||
mod mam;
|
||||
pub use mam::*;
|
||||
@ -49,7 +49,7 @@ use crate::{
|
||||
},
|
||||
api2::types::{
|
||||
VirtualTapeDrive,
|
||||
LinuxTapeDrive,
|
||||
LtoTapeDrive,
|
||||
},
|
||||
server::{
|
||||
send_load_media_email,
|
||||
@ -263,8 +263,8 @@ pub fn media_changer(
|
||||
let tape = VirtualTapeDrive::deserialize(config)?;
|
||||
Ok(Some((Box::new(tape), drive.to_string())))
|
||||
}
|
||||
"linux" => {
|
||||
let drive_config = LinuxTapeDrive::deserialize(config)?;
|
||||
"lto" => {
|
||||
let drive_config = LtoTapeDrive::deserialize(config)?;
|
||||
match drive_config.changer {
|
||||
Some(ref changer_name) => {
|
||||
let changer = MtxMediaChanger::with_drive_config(&drive_config)?;
|
||||
@ -317,8 +317,8 @@ pub fn open_drive(
|
||||
let handle = tape.open()?;
|
||||
Ok(Box::new(handle))
|
||||
}
|
||||
"linux" => {
|
||||
let tape = LinuxTapeDrive::deserialize(config)?;
|
||||
"lto" => {
|
||||
let tape = LtoTapeDrive::deserialize(config)?;
|
||||
let handle = tape.open()?;
|
||||
Ok(Box::new(handle))
|
||||
}
|
||||
@ -379,8 +379,8 @@ pub fn request_and_load_media(
|
||||
|
||||
Ok((handle, media_id))
|
||||
}
|
||||
"linux" => {
|
||||
let drive_config = LinuxTapeDrive::deserialize(config)?;
|
||||
"lto" => {
|
||||
let drive_config = LtoTapeDrive::deserialize(config)?;
|
||||
|
||||
let label_text = label.label_text.clone();
|
||||
|
||||
@ -546,8 +546,8 @@ fn tape_device_path(
|
||||
"virtual" => {
|
||||
VirtualTapeDrive::deserialize(config)?.path
|
||||
}
|
||||
"linux" => {
|
||||
LinuxTapeDrive::deserialize(config)?.path
|
||||
"lto" => {
|
||||
LtoTapeDrive::deserialize(config)?.path
|
||||
}
|
||||
_ => bail!("unknown drive type '{}' - internal error"),
|
||||
};
|
||||
|
@ -12,14 +12,14 @@ use crate::{
|
||||
tools::fs::scan_subdir,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static!{
|
||||
static ref SCSI_GENERIC_NAME_REGEX: regex::Regex =
|
||||
regex::Regex::new(r"^sg\d+$").unwrap();
|
||||
}
|
||||
|
||||
/// List linux tape changer devices
|
||||
pub fn linux_tape_changer_list() -> Vec<TapeDeviceInfo> {
|
||||
|
||||
lazy_static::lazy_static!{
|
||||
static ref SCSI_GENERIC_NAME_REGEX: regex::Regex =
|
||||
regex::Regex::new(r"^sg\d+$").unwrap();
|
||||
}
|
||||
|
||||
let mut list = Vec::new();
|
||||
|
||||
let dir_iter = match scan_subdir(
|
||||
@ -111,20 +111,15 @@ pub fn linux_tape_changer_list() -> Vec<TapeDeviceInfo> {
|
||||
list
|
||||
}
|
||||
|
||||
/// List linux tape devices (non-rewinding)
|
||||
pub fn linux_tape_device_list() -> Vec<TapeDeviceInfo> {
|
||||
|
||||
lazy_static::lazy_static!{
|
||||
static ref NST_TAPE_NAME_REGEX: regex::Regex =
|
||||
regex::Regex::new(r"^nst\d+$").unwrap();
|
||||
}
|
||||
/// List LTO drives
|
||||
pub fn lto_tape_device_list() -> Vec<TapeDeviceInfo> {
|
||||
|
||||
let mut list = Vec::new();
|
||||
|
||||
let dir_iter = match scan_subdir(
|
||||
libc::AT_FDCWD,
|
||||
"/sys/class/scsi_tape",
|
||||
&NST_TAPE_NAME_REGEX)
|
||||
"/sys/class/scsi_generic",
|
||||
&SCSI_GENERIC_NAME_REGEX)
|
||||
{
|
||||
Err(_) => return list,
|
||||
Ok(iter) => iter,
|
||||
@ -138,7 +133,7 @@ pub fn linux_tape_device_list() -> Vec<TapeDeviceInfo> {
|
||||
|
||||
let name = item.file_name().to_str().unwrap().to_string();
|
||||
|
||||
let mut sys_path = PathBuf::from("/sys/class/scsi_tape");
|
||||
let mut sys_path = PathBuf::from("/sys/class/scsi_generic");
|
||||
sys_path.push(&name);
|
||||
|
||||
let device = match udev::Device::from_syspath(&sys_path) {
|
||||
@ -151,6 +146,24 @@ pub fn linux_tape_device_list() -> Vec<TapeDeviceInfo> {
|
||||
Some(devnum) => devnum,
|
||||
};
|
||||
|
||||
let parent = match device.parent() {
|
||||
None => continue,
|
||||
Some(parent) => parent,
|
||||
};
|
||||
|
||||
match parent.attribute_value("type") {
|
||||
Some(type_osstr) => {
|
||||
if type_osstr != "1" {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => { continue; }
|
||||
}
|
||||
|
||||
// let mut test_path = sys_path.clone();
|
||||
// test_path.push("device/scsi_tape");
|
||||
// if !test_path.exists() { continue; }
|
||||
|
||||
let _dev_path = match device.devnode().map(Path::to_owned) {
|
||||
None => continue,
|
||||
Some(dev_path) => dev_path,
|
||||
@ -174,7 +187,7 @@ pub fn linux_tape_device_list() -> Vec<TapeDeviceInfo> {
|
||||
.and_then(|s| if let Ok(s) = s.into_string() { Some(s) } else { None })
|
||||
.unwrap_or_else(|| String::from("unknown"));
|
||||
|
||||
let dev_path = format!("/dev/tape/by-id/scsi-{}-nst", serial);
|
||||
let dev_path = format!("/dev/tape/by-id/scsi-{}-sg", serial);
|
||||
|
||||
if PathBuf::from(&dev_path).exists() {
|
||||
list.push(TapeDeviceInfo {
|
||||
@ -230,13 +243,13 @@ pub fn lookup_device_identification<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Make sure path is a linux tape device
|
||||
/// Make sure path is a lto tape device
|
||||
pub fn check_drive_path(
|
||||
drives: &[TapeDeviceInfo],
|
||||
path: &str,
|
||||
) -> Result<(), Error> {
|
||||
if lookup_device(drives, path).is_none() {
|
||||
bail!("path '{}' is not a linux (non-rewinding) tape device", path);
|
||||
bail!("path '{}' is not a lto SCSI-generic tape device", path);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -250,5 +263,5 @@ pub fn complete_changer_path(_arg: &str, _param: &HashMap<String, String>) -> Ve
|
||||
|
||||
/// List tape device paths
|
||||
pub fn complete_drive_path(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
|
||||
linux_tape_device_list().iter().map(|v| v.path.clone()).collect()
|
||||
lto_tape_device_list().iter().map(|v| v.path.clone()).collect()
|
||||
}
|
||||
|
Reference in New Issue
Block a user