split tape code into new pbs_tape workspace
This commit is contained in:
25
pbs-tape/Cargo.toml
Normal file
25
pbs-tape/Cargo.toml
Normal file
@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "pbs-tape"
|
||||
version = "0.1.0"
|
||||
authors = ["Proxmox Support Team <support@proxmox.com>"]
|
||||
edition = "2018"
|
||||
description = "LTO tage support"
|
||||
|
||||
[dependencies]
|
||||
lazy_static = "1.4"
|
||||
libc = "0.2"
|
||||
anyhow = "1.0"
|
||||
thiserror = "1.0"
|
||||
endian_trait = { version = "0.6", features = ["arrays"] }
|
||||
nix = "0.19.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
bitflags = "1.2.1"
|
||||
regex = "1.2"
|
||||
udev = ">= 0.3, <0.5"
|
||||
|
||||
proxmox = { version = "0.13.0", default-features = false, features = [] }
|
||||
|
||||
pbs-api-types = { path = "../pbs-api-types" }
|
||||
pbs-tools = { path = "../pbs-tools" }
|
||||
pbs-config = { path = "../pbs-config" }
|
897
pbs-tape/src/bin/pmt.rs
Normal file
897
pbs-tape/src/bin/pmt.rs
Normal file
@ -0,0 +1,897 @@
|
||||
/// Control magnetic tape drive operation
|
||||
///
|
||||
/// This is a Rust implementation, using the Proxmox userspace tape
|
||||
/// driver. This is meant as replacement fot the 'mt' command line
|
||||
/// tool.
|
||||
///
|
||||
/// Features:
|
||||
///
|
||||
/// - written in Rust
|
||||
/// - use Proxmox userspace driver (using SG_IO)
|
||||
/// - optional json output format
|
||||
/// - support tape alert flags
|
||||
/// - support volume statistics
|
||||
/// - read cartridge memory
|
||||
|
||||
use std::convert::TryInto;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use serde_json::Value;
|
||||
|
||||
use proxmox::{
|
||||
api::{
|
||||
api,
|
||||
cli::*,
|
||||
schema::{
|
||||
Schema,
|
||||
IntegerSchema,
|
||||
StringSchema,
|
||||
ArraySchema,
|
||||
},
|
||||
RpcEnvironment,
|
||||
},
|
||||
};
|
||||
|
||||
use pbs_api_types::{
|
||||
LTO_DRIVE_PATH_SCHEMA, DRIVE_NAME_SCHEMA, LtoTapeDrive,
|
||||
};
|
||||
use pbs_config::drive::complete_drive_name;
|
||||
use pbs_tape::{
|
||||
sg_tape::SgTape,
|
||||
linux_list_drives::{complete_drive_path, lto_tape_device_list, open_lto_tape_device},
|
||||
};
|
||||
|
||||
pub const FILE_MARK_COUNT_SCHEMA: Schema =
|
||||
IntegerSchema::new("File mark count.")
|
||||
.minimum(1)
|
||||
.maximum(i32::MAX as isize)
|
||||
.schema();
|
||||
|
||||
pub const FILE_MARK_POSITION_SCHEMA: Schema =
|
||||
IntegerSchema::new("File mark position (0 is BOT).")
|
||||
.minimum(0)
|
||||
.maximum(i32::MAX as isize)
|
||||
.schema();
|
||||
|
||||
pub const RECORD_COUNT_SCHEMA: Schema =
|
||||
IntegerSchema::new("Record count.")
|
||||
.minimum(1)
|
||||
.maximum(i32::MAX as isize)
|
||||
.schema();
|
||||
|
||||
pub const DRIVE_OPTION_SCHEMA: Schema = StringSchema::new(
|
||||
"Lto Tape Driver Option, either numeric value or option name.")
|
||||
.schema();
|
||||
|
||||
pub const DRIVE_OPTION_LIST_SCHEMA: Schema =
|
||||
ArraySchema::new("Drive Option List.", &DRIVE_OPTION_SCHEMA)
|
||||
.min_length(1)
|
||||
.schema();
|
||||
|
||||
fn get_tape_handle(param: &Value) -> Result<SgTape, Error> {
|
||||
|
||||
if let Some(name) = param["drive"].as_str() {
|
||||
let (config, _digest) = pbs_config::drive::config()?;
|
||||
let drive: LtoTapeDrive = config.lookup("lto", &name)?;
|
||||
eprintln!("using device {}", drive.path);
|
||||
return SgTape::new(open_lto_tape_device(&drive.path)?);
|
||||
}
|
||||
|
||||
if let Some(device) = param["device"].as_str() {
|
||||
eprintln!("using device {}", device);
|
||||
return SgTape::new(open_lto_tape_device(&device)?);
|
||||
}
|
||||
|
||||
if let Ok(name) = std::env::var("PROXMOX_TAPE_DRIVE") {
|
||||
let (config, _digest) = pbs_config::drive::config()?;
|
||||
let drive: LtoTapeDrive = config.lookup("lto", &name)?;
|
||||
eprintln!("using device {}", drive.path);
|
||||
return SgTape::new(open_lto_tape_device(&drive.path)?);
|
||||
}
|
||||
|
||||
if let Ok(device) = std::env::var("TAPE") {
|
||||
eprintln!("using device {}", device);
|
||||
return SgTape::new(open_lto_tape_device(&device)?);
|
||||
}
|
||||
|
||||
let (config, _digest) = pbs_config::drive::config()?;
|
||||
|
||||
let mut drive_names = Vec::new();
|
||||
for (name, (section_type, _)) in config.sections.iter() {
|
||||
if section_type != "lto" { continue; }
|
||||
drive_names.push(name);
|
||||
}
|
||||
|
||||
if drive_names.len() == 1 {
|
||||
let name = drive_names[0];
|
||||
let drive: LtoTapeDrive = config.lookup("lto", &name)?;
|
||||
eprintln!("using device {}", drive.path);
|
||||
return SgTape::new(open_lto_tape_device(&drive.path)?);
|
||||
}
|
||||
|
||||
bail!("no drive/device specified");
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
count: {
|
||||
schema: FILE_MARK_POSITION_SCHEMA,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Position the tape at the beginning of the count file (after
|
||||
/// filemark count)
|
||||
fn asf(count: u64, param: Value) -> Result<(), Error> {
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
|
||||
handle.locate_file(count)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
count: {
|
||||
schema: FILE_MARK_COUNT_SCHEMA,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Backward space count files (position before file mark).
|
||||
///
|
||||
/// The tape is positioned on the last block of the previous file.
|
||||
fn bsf(count: usize, param: Value) -> Result<(), Error> {
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
|
||||
handle.space_filemarks(-count.try_into()?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
count: {
|
||||
schema: FILE_MARK_COUNT_SCHEMA,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Backward space count files, then forward space one record (position after file mark).
|
||||
///
|
||||
/// This leaves the tape positioned at the first block of the file
|
||||
/// that is count - 1 files before the current file.
|
||||
fn bsfm(count: usize, param: Value) -> Result<(), Error> {
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
|
||||
handle.space_filemarks(-count.try_into()?)?;
|
||||
handle.space_filemarks(1)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
count: {
|
||||
schema: RECORD_COUNT_SCHEMA,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Backward space records.
|
||||
fn bsr(count: usize, param: Value) -> Result<(), Error> {
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
|
||||
handle.space_blocks(-count.try_into()?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
"output-format": {
|
||||
schema: OUTPUT_FORMAT,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Read Cartridge Memory
|
||||
fn cartridge_memory(param: Value) -> Result<(), Error> {
|
||||
|
||||
let output_format = get_output_format(¶m);
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
let result = handle.cartridge_memory();
|
||||
|
||||
if output_format == "json-pretty" {
|
||||
let result = result.map_err(|err: Error| err.to_string());
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if output_format == "json" {
|
||||
let result = result.map_err(|err: Error| err.to_string());
|
||||
println!("{}", serde_json::to_string(&result)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if output_format != "text" {
|
||||
bail!("unknown output format '{}'", output_format);
|
||||
}
|
||||
|
||||
let list = result?;
|
||||
|
||||
for item in list {
|
||||
println!("{}|{}|{}", item.id, item.name, item.value);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
"output-format": {
|
||||
schema: OUTPUT_FORMAT,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Read Tape Alert Flags
|
||||
fn tape_alert_flags(param: Value) -> Result<(), Error> {
|
||||
|
||||
let output_format = get_output_format(¶m);
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
let result = handle.tape_alert_flags()
|
||||
.map(|flags| format!("{:?}", flags));
|
||||
|
||||
if output_format == "json-pretty" {
|
||||
let result = result.map_err(|err: Error| err.to_string());
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if output_format == "json" {
|
||||
let result = result.map_err(|err: Error| err.to_string());
|
||||
println!("{}", serde_json::to_string(&result)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if output_format != "text" {
|
||||
bail!("unknown output format '{}'", output_format);
|
||||
}
|
||||
|
||||
let flags = result?;
|
||||
println!("Tape Alert Flags: {}", flags);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Eject drive media
|
||||
fn eject(param: Value) -> Result<(), Error> {
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
handle.eject()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Move to end of media
|
||||
fn eod(param: Value) -> Result<(), Error> {
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
handle.move_to_eom(false)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
fast: {
|
||||
description: "Use fast erase.",
|
||||
type: bool,
|
||||
optional: true,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Erase media (from current position)
|
||||
fn erase(fast: Option<bool>, param: Value) -> Result<(), Error> {
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
handle.erase_media(fast.unwrap_or(true))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
fast: {
|
||||
description: "Use fast erase.",
|
||||
type: bool,
|
||||
optional: true,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Format media, single partition
|
||||
fn format(fast: Option<bool>, param: Value) -> Result<(), Error> {
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
handle.format_media(fast.unwrap_or(true))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
count: {
|
||||
schema: FILE_MARK_COUNT_SCHEMA,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Forward space count files (position after file mark).
|
||||
///
|
||||
/// The tape is positioned on the first block of the next file.
|
||||
fn fsf(count: usize, param: Value) -> Result<(), Error> {
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
|
||||
handle.space_filemarks(count.try_into()?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
count: {
|
||||
schema: FILE_MARK_COUNT_SCHEMA,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Forward space count files, then backward space one record (position before file mark).
|
||||
///
|
||||
/// This leaves the tape positioned at the last block of the file that
|
||||
/// is count - 1 files past the current file.
|
||||
fn fsfm(count: usize, param: Value) -> Result<(), Error> {
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
|
||||
handle.space_filemarks(count.try_into()?)?;
|
||||
handle.space_filemarks(-1)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
count: {
|
||||
schema: RECORD_COUNT_SCHEMA,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Forward space records.
|
||||
fn fsr(count: usize, param: Value) -> Result<(), Error> {
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
|
||||
handle.space_blocks(count.try_into()?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Load media
|
||||
fn load(param: Value) -> Result<(), Error> {
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
handle.load()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Lock the tape drive door
|
||||
fn lock(param: Value) -> Result<(), Error> {
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
|
||||
handle.set_medium_removal(false)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Rewind the tape
|
||||
fn rewind(param: Value) -> Result<(), Error> {
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
handle.rewind()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
"output-format": {
|
||||
schema: OUTPUT_FORMAT,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Scan for existing tape changer devices
|
||||
fn scan(param: Value) -> Result<(), Error> {
|
||||
|
||||
let output_format = get_output_format(¶m);
|
||||
|
||||
let list = lto_tape_device_list();
|
||||
|
||||
if output_format == "json-pretty" {
|
||||
println!("{}", serde_json::to_string_pretty(&list)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if output_format == "json" {
|
||||
println!("{}", serde_json::to_string(&list)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if output_format != "text" {
|
||||
bail!("unknown output format '{}'", output_format);
|
||||
}
|
||||
|
||||
for item in list.iter() {
|
||||
println!("{} ({}/{}/{})", item.path, item.vendor, item.model, item.serial);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
"output-format": {
|
||||
schema: OUTPUT_FORMAT,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Drive Status
|
||||
fn status(param: Value) -> Result<(), Error> {
|
||||
|
||||
let output_format = get_output_format(¶m);
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
|
||||
let result = handle.get_drive_and_media_status();
|
||||
|
||||
if output_format == "json-pretty" {
|
||||
let result = result.map_err(|err: Error| err.to_string());
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if output_format == "json" {
|
||||
let result = result.map_err(|err: Error| err.to_string());
|
||||
println!("{}", serde_json::to_string(&result)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if output_format != "text" {
|
||||
bail!("unknown output format '{}'", output_format);
|
||||
}
|
||||
|
||||
let status = result?;
|
||||
|
||||
println!("{}", serde_json::to_string_pretty(&status)?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Unlock the tape drive door
|
||||
fn unlock(param: Value) -> Result<(), Error> {
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
|
||||
handle.set_medium_removal(true)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
"output-format": {
|
||||
schema: OUTPUT_FORMAT,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Volume Statistics
|
||||
fn volume_statistics(param: Value) -> Result<(), Error> {
|
||||
|
||||
let output_format = get_output_format(¶m);
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
let result = handle.volume_statistics();
|
||||
|
||||
if output_format == "json-pretty" {
|
||||
let result = result.map_err(|err: Error| err.to_string());
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if output_format == "json" {
|
||||
let result = result.map_err(|err: Error| err.to_string());
|
||||
println!("{}", serde_json::to_string(&result)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if output_format != "text" {
|
||||
bail!("unknown output format '{}'", output_format);
|
||||
}
|
||||
|
||||
let data = result?;
|
||||
|
||||
println!("{}", serde_json::to_string_pretty(&data)?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
count: {
|
||||
schema: FILE_MARK_COUNT_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Write count (default 1) EOF marks at current position.
|
||||
fn weof(count: Option<usize>, param: Value) -> Result<(), Error> {
|
||||
|
||||
let count = count.unwrap_or(1);
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
|
||||
handle.write_filemarks(count, false)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
drive: {
|
||||
schema: DRIVE_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: LTO_DRIVE_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
compression: {
|
||||
description: "Enable/disable compression.",
|
||||
type: bool,
|
||||
optional: true,
|
||||
},
|
||||
blocksize: {
|
||||
description: "Set tape drive block_length (0 is variable length).",
|
||||
type: u32,
|
||||
minimum: 0,
|
||||
maximum: 0x80_00_00,
|
||||
optional: true,
|
||||
},
|
||||
buffer_mode: {
|
||||
description: "Use drive buffer.",
|
||||
type: bool,
|
||||
optional: true,
|
||||
},
|
||||
defaults: {
|
||||
description: "Set default options",
|
||||
type: bool,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Set varios drive options
|
||||
fn options(
|
||||
compression: Option<bool>,
|
||||
blocksize: Option<u32>,
|
||||
buffer_mode: Option<bool>,
|
||||
defaults: Option<bool>,
|
||||
param: Value,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let mut handle = get_tape_handle(¶m)?;
|
||||
|
||||
if let Some(true) = defaults {
|
||||
handle.set_default_options()?;
|
||||
}
|
||||
|
||||
handle.set_drive_options(compression, blocksize, buffer_mode)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
|
||||
let uid = nix::unistd::Uid::current();
|
||||
|
||||
let username = match nix::unistd::User::from_uid(uid)? {
|
||||
Some(user) => user.name,
|
||||
None => bail!("unable to get user name"),
|
||||
};
|
||||
|
||||
let std_cmd = |method| {
|
||||
CliCommand::new(method)
|
||||
.completion_cb("drive", complete_drive_name)
|
||||
.completion_cb("device", complete_drive_path)
|
||||
};
|
||||
|
||||
let cmd_def = CliCommandMap::new()
|
||||
.usage_skip_options(&["device", "drive", "output-format"])
|
||||
.insert("asf", std_cmd(&API_METHOD_ASF).arg_param(&["count"]))
|
||||
.insert("bsf", std_cmd(&API_METHOD_BSF).arg_param(&["count"]))
|
||||
.insert("bsfm", std_cmd(&API_METHOD_BSFM).arg_param(&["count"]))
|
||||
.insert("bsr", std_cmd(&API_METHOD_BSR).arg_param(&["count"]))
|
||||
.insert("cartridge-memory", std_cmd(&API_METHOD_CARTRIDGE_MEMORY))
|
||||
.insert("eject", std_cmd(&API_METHOD_EJECT))
|
||||
.insert("eod", std_cmd(&API_METHOD_EOD))
|
||||
.insert("erase", std_cmd(&API_METHOD_ERASE))
|
||||
.insert("format", std_cmd(&API_METHOD_FORMAT))
|
||||
.insert("fsf", std_cmd(&API_METHOD_FSF).arg_param(&["count"]))
|
||||
.insert("fsfm", std_cmd(&API_METHOD_FSFM).arg_param(&["count"]))
|
||||
.insert("fsr", std_cmd(&API_METHOD_FSR).arg_param(&["count"]))
|
||||
.insert("load", std_cmd(&API_METHOD_LOAD))
|
||||
.insert("lock", std_cmd(&API_METHOD_LOCK))
|
||||
.insert("options", std_cmd(&API_METHOD_OPTIONS))
|
||||
.insert("rewind", std_cmd(&API_METHOD_REWIND))
|
||||
.insert("scan", CliCommand::new(&API_METHOD_SCAN))
|
||||
.insert("status", std_cmd(&API_METHOD_STATUS))
|
||||
.insert("tape-alert-flags", std_cmd(&API_METHOD_TAPE_ALERT_FLAGS))
|
||||
.insert("unlock", std_cmd(&API_METHOD_UNLOCK))
|
||||
.insert("volume-statistics", std_cmd(&API_METHOD_VOLUME_STATISTICS))
|
||||
.insert("weof", std_cmd(&API_METHOD_WEOF).arg_param(&["count"]))
|
||||
;
|
||||
|
||||
let mut rpcenv = CliEnvironment::new();
|
||||
rpcenv.set_auth_id(Some(format!("{}@pam", username)));
|
||||
|
||||
run_cli_command(cmd_def, rpcenv, None);
|
||||
|
||||
Ok(())
|
||||
}
|
472
pbs-tape/src/bin/pmtx.rs
Normal file
472
pbs-tape/src/bin/pmtx.rs
Normal file
@ -0,0 +1,472 @@
|
||||
/// SCSI changer command implemented using scsi-generic raw commands
|
||||
///
|
||||
/// This is a Rust implementation, meant to replace the 'mtx' command
|
||||
/// line tool.
|
||||
///
|
||||
/// Features:
|
||||
///
|
||||
/// - written in Rust
|
||||
///
|
||||
/// - json output
|
||||
///
|
||||
/// - list serial number for attached drives, so that it is possible
|
||||
/// to associate drive numbers with drives.
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use serde_json::Value;
|
||||
|
||||
use proxmox::{
|
||||
api::{
|
||||
api,
|
||||
cli::*,
|
||||
RpcEnvironment,
|
||||
},
|
||||
};
|
||||
|
||||
use pbs_config::drive::complete_changer_name;
|
||||
use pbs_api_types::{
|
||||
SCSI_CHANGER_PATH_SCHEMA, CHANGER_NAME_SCHEMA, ScsiTapeChanger, LtoTapeDrive,
|
||||
};
|
||||
use pbs_tape::{
|
||||
sgutils2::scsi_inquiry,
|
||||
ElementStatus,
|
||||
sg_pt_changer,
|
||||
linux_list_drives::{complete_changer_path, linux_tape_changer_list},
|
||||
};
|
||||
|
||||
fn get_changer_handle(param: &Value) -> Result<File, Error> {
|
||||
|
||||
if let Some(name) = param["changer"].as_str() {
|
||||
let (config, _digest) = pbs_config::drive::config()?;
|
||||
let changer_config: ScsiTapeChanger = config.lookup("changer", &name)?;
|
||||
eprintln!("using device {}", changer_config.path);
|
||||
return sg_pt_changer::open(&changer_config.path);
|
||||
}
|
||||
|
||||
if let Some(device) = param["device"].as_str() {
|
||||
eprintln!("using device {}", device);
|
||||
return sg_pt_changer::open(device);
|
||||
}
|
||||
|
||||
if let Ok(name) = std::env::var("PROXMOX_TAPE_DRIVE") {
|
||||
let (config, _digest) = pbs_config::drive::config()?;
|
||||
let drive: LtoTapeDrive = config.lookup("lto", &name)?;
|
||||
if let Some(changer) = drive.changer {
|
||||
let changer_config: ScsiTapeChanger = config.lookup("changer", &changer)?;
|
||||
eprintln!("using device {}", changer_config.path);
|
||||
return sg_pt_changer::open(&changer_config.path);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(device) = std::env::var("CHANGER") {
|
||||
eprintln!("using device {}", device);
|
||||
return sg_pt_changer::open(device);
|
||||
}
|
||||
|
||||
bail!("no changer device specified");
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
changer: {
|
||||
schema: CHANGER_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: SCSI_CHANGER_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
"output-format": {
|
||||
schema: OUTPUT_FORMAT,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Inquiry
|
||||
fn inquiry(
|
||||
param: Value,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let output_format = get_output_format(¶m);
|
||||
|
||||
let result: Result<_, Error> = proxmox::try_block!({
|
||||
let mut file = get_changer_handle(¶m)?;
|
||||
let info = scsi_inquiry(&mut file)?;
|
||||
Ok(info)
|
||||
});
|
||||
|
||||
if output_format == "json-pretty" {
|
||||
let result = result.map_err(|err: Error| err.to_string());
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if output_format == "json" {
|
||||
let result = result.map_err(|err: Error| err.to_string());
|
||||
println!("{}", serde_json::to_string(&result)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if output_format != "text" {
|
||||
bail!("unknown output format '{}'", output_format);
|
||||
}
|
||||
|
||||
let info = result?;
|
||||
|
||||
println!("Type: {} ({})", info.peripheral_type_text, info.peripheral_type);
|
||||
println!("Vendor: {}", info.vendor);
|
||||
println!("Product: {}", info.product);
|
||||
println!("Revision: {}", info.revision);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
changer: {
|
||||
schema: CHANGER_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: SCSI_CHANGER_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Inventory
|
||||
fn inventory(
|
||||
param: Value,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let mut file = get_changer_handle(¶m)?;
|
||||
sg_pt_changer::initialize_element_status(&mut file)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
changer: {
|
||||
schema: CHANGER_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: SCSI_CHANGER_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
slot: {
|
||||
description: "Storage slot number (source).",
|
||||
type: u64,
|
||||
},
|
||||
drivenum: {
|
||||
description: "Target drive number (defaults to Drive 0)",
|
||||
type: u64,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Load
|
||||
fn load(
|
||||
param: Value,
|
||||
slot: u64,
|
||||
drivenum: Option<u64>,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let mut file = get_changer_handle(¶m)?;
|
||||
|
||||
let drivenum = drivenum.unwrap_or(0);
|
||||
|
||||
sg_pt_changer::load_slot(&mut file, slot, drivenum)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
changer: {
|
||||
schema: CHANGER_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: SCSI_CHANGER_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
slot: {
|
||||
description: "Storage slot number (target). If omitted, defaults to the slot that the drive was loaded from.",
|
||||
type: u64,
|
||||
optional: true,
|
||||
},
|
||||
drivenum: {
|
||||
description: "Target drive number (defaults to Drive 0)",
|
||||
type: u64,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Unload
|
||||
fn unload(
|
||||
param: Value,
|
||||
slot: Option<u64>,
|
||||
drivenum: Option<u64>,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let mut file = get_changer_handle(¶m)?;
|
||||
|
||||
let drivenum = drivenum.unwrap_or(0);
|
||||
|
||||
if let Some(to_slot) = slot {
|
||||
sg_pt_changer::unload(&mut file, to_slot, drivenum)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let status = sg_pt_changer::read_element_status(&mut file)?;
|
||||
|
||||
if let Some(info) = status.drives.get(drivenum as usize) {
|
||||
if let ElementStatus::Empty = info.status {
|
||||
bail!("Drive {} is empty.", drivenum);
|
||||
}
|
||||
if let Some(to_slot) = info.loaded_slot {
|
||||
// check if original slot is empty/usable
|
||||
if let Some(slot_info) = status.slots.get(to_slot as usize - 1) {
|
||||
if let ElementStatus::Empty = slot_info.status {
|
||||
sg_pt_changer::unload(&mut file, to_slot, drivenum)?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(to_slot) = status.find_free_slot(false) {
|
||||
sg_pt_changer::unload(&mut file, to_slot, drivenum)?;
|
||||
return Ok(());
|
||||
} else {
|
||||
bail!("Drive '{}' unload failure - no free slot", drivenum);
|
||||
}
|
||||
} else {
|
||||
bail!("Drive {} does not exist.", drivenum);
|
||||
}
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
changer: {
|
||||
schema: CHANGER_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: SCSI_CHANGER_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
"output-format": {
|
||||
schema: OUTPUT_FORMAT,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Changer Status
|
||||
fn status(
|
||||
param: Value,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let output_format = get_output_format(¶m);
|
||||
|
||||
let result: Result<_, Error> = proxmox::try_block!({
|
||||
let mut file = get_changer_handle(¶m)?;
|
||||
let status = sg_pt_changer::read_element_status(&mut file)?;
|
||||
Ok(status)
|
||||
});
|
||||
|
||||
if output_format == "json-pretty" {
|
||||
let result = result.map_err(|err: Error| err.to_string());
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if output_format == "json" {
|
||||
let result = result.map_err(|err: Error| err.to_string());
|
||||
println!("{}", serde_json::to_string(&result)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if output_format != "text" {
|
||||
bail!("unknown output format '{}'", output_format);
|
||||
}
|
||||
|
||||
let status = result?;
|
||||
|
||||
for (i, transport) in status.transports.iter().enumerate() {
|
||||
println!("Transport Element (Griper) {:>3}: {:?}",i, transport.status);
|
||||
}
|
||||
|
||||
for (i, drive) in status.drives.iter().enumerate() {
|
||||
let loaded_txt = match drive.loaded_slot {
|
||||
Some(slot) => format!(", Source: {}", slot),
|
||||
None => String::new(),
|
||||
};
|
||||
let serial_txt = match drive.drive_serial_number {
|
||||
Some(ref serial) => format!(", Serial: {}", serial),
|
||||
None => String::new(),
|
||||
};
|
||||
|
||||
println!(
|
||||
"Data Transfer Element (Drive) {:>3}: {:?}{}{}",
|
||||
i, drive.status, loaded_txt, serial_txt,
|
||||
);
|
||||
}
|
||||
|
||||
for (i, slot) in status.slots.iter().enumerate() {
|
||||
if slot.import_export {
|
||||
println!(" Import/Export {:>3}: {:?}", i+1, slot.status);
|
||||
} else {
|
||||
println!(" Storage Element {:>3}: {:?}", i+1, slot.status);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
changer: {
|
||||
schema: CHANGER_NAME_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
device: {
|
||||
schema: SCSI_CHANGER_PATH_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
from: {
|
||||
description: "Source storage slot number.",
|
||||
type: u64,
|
||||
},
|
||||
to: {
|
||||
description: "Target storage slot number.",
|
||||
type: u64,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Transfer
|
||||
fn transfer(
|
||||
param: Value,
|
||||
from: u64,
|
||||
to: u64,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let mut file = get_changer_handle(¶m)?;
|
||||
|
||||
sg_pt_changer::transfer_medium(&mut file, from, to)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
"output-format": {
|
||||
schema: OUTPUT_FORMAT,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Scan for existing tape changer devices
|
||||
fn scan(param: Value) -> Result<(), Error> {
|
||||
|
||||
let output_format = get_output_format(¶m);
|
||||
|
||||
let list = linux_tape_changer_list();
|
||||
|
||||
if output_format == "json-pretty" {
|
||||
println!("{}", serde_json::to_string_pretty(&list)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if output_format == "json" {
|
||||
println!("{}", serde_json::to_string(&list)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if output_format != "text" {
|
||||
bail!("unknown output format '{}'", output_format);
|
||||
}
|
||||
|
||||
for item in list.iter() {
|
||||
println!("{} ({}/{}/{})", item.path, item.vendor, item.model, item.serial);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
|
||||
let uid = nix::unistd::Uid::current();
|
||||
|
||||
let username = match nix::unistd::User::from_uid(uid)? {
|
||||
Some(user) => user.name,
|
||||
None => bail!("unable to get user name"),
|
||||
};
|
||||
|
||||
|
||||
let cmd_def = CliCommandMap::new()
|
||||
.usage_skip_options(&["device", "changer", "output-format"])
|
||||
.insert(
|
||||
"inquiry",
|
||||
CliCommand::new(&API_METHOD_INQUIRY)
|
||||
.completion_cb("changer", complete_changer_name)
|
||||
.completion_cb("device", complete_changer_path)
|
||||
)
|
||||
.insert(
|
||||
"inventory",
|
||||
CliCommand::new(&API_METHOD_INVENTORY)
|
||||
.completion_cb("changer", complete_changer_name)
|
||||
.completion_cb("device", complete_changer_path)
|
||||
)
|
||||
.insert(
|
||||
"load",
|
||||
CliCommand::new(&API_METHOD_LOAD)
|
||||
.arg_param(&["slot"])
|
||||
.completion_cb("changer", complete_changer_name)
|
||||
.completion_cb("device", complete_changer_path)
|
||||
)
|
||||
.insert(
|
||||
"unload",
|
||||
CliCommand::new(&API_METHOD_UNLOAD)
|
||||
.completion_cb("changer", complete_changer_name)
|
||||
.completion_cb("device", complete_changer_path)
|
||||
)
|
||||
.insert("scan", CliCommand::new(&API_METHOD_SCAN))
|
||||
.insert(
|
||||
"status",
|
||||
CliCommand::new(&API_METHOD_STATUS)
|
||||
.completion_cb("changer", complete_changer_name)
|
||||
.completion_cb("device", complete_changer_path)
|
||||
)
|
||||
.insert(
|
||||
"transfer",
|
||||
CliCommand::new(&API_METHOD_TRANSFER)
|
||||
.arg_param(&["from", "to"])
|
||||
.completion_cb("changer", complete_changer_name)
|
||||
.completion_cb("device", complete_changer_path)
|
||||
)
|
||||
;
|
||||
|
||||
let mut rpcenv = CliEnvironment::new();
|
||||
rpcenv.set_auth_id(Some(format!("{}@pam", username)));
|
||||
|
||||
run_cli_command(cmd_def, rpcenv, None);
|
||||
|
||||
Ok(())
|
||||
}
|
370
pbs-tape/src/blocked_reader.rs
Normal file
370
pbs-tape/src/blocked_reader.rs
Normal file
@ -0,0 +1,370 @@
|
||||
use std::io::Read;
|
||||
|
||||
use crate::{
|
||||
TapeRead,
|
||||
BlockRead,
|
||||
BlockReadError,
|
||||
PROXMOX_TAPE_BLOCK_HEADER_MAGIC_1_0,
|
||||
BlockHeader,
|
||||
BlockHeaderFlags,
|
||||
};
|
||||
|
||||
/// Read a block stream generated by 'BlockWriter'.
|
||||
///
|
||||
/// This class implements 'TapeRead'. It always read whole blocks from
|
||||
/// the underlying reader, and does additional error checks:
|
||||
///
|
||||
/// - check magic number (detect streams not written by 'BlockWriter')
|
||||
/// - check block size
|
||||
/// - check block sequence numbers
|
||||
///
|
||||
/// The reader consumes the EOF mark after the data stream (if read to
|
||||
/// the end of the stream).
|
||||
pub struct BlockedReader<R> {
|
||||
reader: R,
|
||||
buffer: Box<BlockHeader>,
|
||||
seq_nr: u32,
|
||||
found_end_marker: bool,
|
||||
incomplete: bool,
|
||||
got_eod: bool,
|
||||
read_error: bool,
|
||||
read_pos: usize,
|
||||
}
|
||||
|
||||
impl <R: BlockRead> BlockedReader<R> {
|
||||
|
||||
/// Create a new BlockedReader instance.
|
||||
///
|
||||
/// This tries to read the first block. Please inspect the error
|
||||
/// to detect EOF and EOT.
|
||||
pub fn open(mut reader: R) -> Result<Self, BlockReadError> {
|
||||
|
||||
let mut buffer = BlockHeader::new();
|
||||
|
||||
Self::read_block_frame(&mut buffer, &mut reader)?;
|
||||
|
||||
let (_size, found_end_marker) = Self::check_buffer(&buffer, 0)?;
|
||||
|
||||
let mut incomplete = false;
|
||||
let mut got_eod = false;
|
||||
|
||||
if found_end_marker {
|
||||
incomplete = buffer.flags.contains(BlockHeaderFlags::INCOMPLETE);
|
||||
Self::consume_eof_marker(&mut reader)?;
|
||||
got_eod = true;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
reader,
|
||||
buffer,
|
||||
found_end_marker,
|
||||
incomplete,
|
||||
got_eod,
|
||||
seq_nr: 1,
|
||||
read_error: false,
|
||||
read_pos: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn check_buffer(buffer: &BlockHeader, seq_nr: u32) -> Result<(usize, bool), std::io::Error> {
|
||||
|
||||
if buffer.magic != PROXMOX_TAPE_BLOCK_HEADER_MAGIC_1_0 {
|
||||
proxmox::io_bail!("detected tape block with wrong magic number - not written by proxmox tape");
|
||||
}
|
||||
|
||||
if seq_nr != buffer.seq_nr() {
|
||||
proxmox::io_bail!(
|
||||
"detected tape block with wrong sequence number ({} != {})",
|
||||
seq_nr, buffer.seq_nr())
|
||||
}
|
||||
|
||||
let size = buffer.size();
|
||||
let found_end_marker = buffer.flags.contains(BlockHeaderFlags::END_OF_STREAM);
|
||||
|
||||
if size > buffer.payload.len() {
|
||||
proxmox::io_bail!("detected tape block with wrong payload size ({} > {}", size, buffer.payload.len());
|
||||
} else if size == 0 && !found_end_marker {
|
||||
proxmox::io_bail!("detected tape block with zero payload size");
|
||||
}
|
||||
|
||||
|
||||
Ok((size, found_end_marker))
|
||||
}
|
||||
|
||||
fn read_block_frame(buffer: &mut BlockHeader, reader: &mut R) -> Result<(), BlockReadError> {
|
||||
|
||||
let data = unsafe {
|
||||
std::slice::from_raw_parts_mut(
|
||||
(buffer as *mut BlockHeader) as *mut u8,
|
||||
BlockHeader::SIZE,
|
||||
)
|
||||
};
|
||||
|
||||
let bytes = reader.read_block(data)?;
|
||||
|
||||
if bytes != BlockHeader::SIZE {
|
||||
return Err(proxmox::io_format_err!("got wrong block size").into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn consume_eof_marker(reader: &mut R) -> Result<(), std::io::Error> {
|
||||
let mut tmp_buf = [0u8; 512]; // use a small buffer for testing EOF
|
||||
match reader.read_block(&mut tmp_buf) {
|
||||
Ok(_) => {
|
||||
proxmox::io_bail!("detected tape block after block-stream end marker");
|
||||
}
|
||||
Err(BlockReadError::EndOfFile) => {
|
||||
return Ok(());
|
||||
}
|
||||
Err(BlockReadError::EndOfStream) => {
|
||||
proxmox::io_bail!("got unexpected end of tape");
|
||||
}
|
||||
Err(BlockReadError::Error(err)) => {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_block(&mut self, check_end_marker: bool) -> Result<usize, std::io::Error> {
|
||||
|
||||
match Self::read_block_frame(&mut self.buffer, &mut self.reader) {
|
||||
Ok(()) => { /* ok */ }
|
||||
Err(BlockReadError::EndOfFile) => {
|
||||
self.got_eod = true;
|
||||
self.read_pos = self.buffer.payload.len();
|
||||
if !self.found_end_marker && check_end_marker {
|
||||
proxmox::io_bail!("detected tape stream without end marker");
|
||||
}
|
||||
return Ok(0); // EOD
|
||||
}
|
||||
Err(BlockReadError::EndOfStream) => {
|
||||
proxmox::io_bail!("got unexpected end of tape");
|
||||
}
|
||||
Err(BlockReadError::Error(err)) => {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
|
||||
let (size, found_end_marker) = Self::check_buffer(&self.buffer, self.seq_nr)?;
|
||||
self.seq_nr += 1;
|
||||
|
||||
if found_end_marker { // consume EOF mark
|
||||
self.found_end_marker = true;
|
||||
self.incomplete = self.buffer.flags.contains(BlockHeaderFlags::INCOMPLETE);
|
||||
Self::consume_eof_marker(&mut self.reader)?;
|
||||
self.got_eod = true;
|
||||
}
|
||||
|
||||
self.read_pos = 0;
|
||||
|
||||
Ok(size)
|
||||
}
|
||||
}
|
||||
|
||||
impl <R: BlockRead> TapeRead for BlockedReader<R> {
|
||||
|
||||
fn is_incomplete(&self) -> Result<bool, std::io::Error> {
|
||||
if !self.got_eod {
|
||||
proxmox::io_bail!("is_incomplete failed: EOD not reached");
|
||||
}
|
||||
if !self.found_end_marker {
|
||||
proxmox::io_bail!("is_incomplete failed: no end marker found");
|
||||
}
|
||||
|
||||
Ok(self.incomplete)
|
||||
}
|
||||
|
||||
fn has_end_marker(&self) -> Result<bool, std::io::Error> {
|
||||
if !self.got_eod {
|
||||
proxmox::io_bail!("has_end_marker failed: EOD not reached");
|
||||
}
|
||||
|
||||
Ok(self.found_end_marker)
|
||||
}
|
||||
|
||||
// like ReadExt::skip_to_end(), but does not raise an error if the
|
||||
// stream has no end marker.
|
||||
fn skip_data(&mut self) -> Result<usize, std::io::Error> {
|
||||
let mut bytes = 0;
|
||||
let buffer_size = self.buffer.size();
|
||||
let rest = (buffer_size as isize) - (self.read_pos as isize);
|
||||
if rest > 0 {
|
||||
bytes = rest as usize;
|
||||
}
|
||||
loop {
|
||||
if self.got_eod {
|
||||
return Ok(bytes);
|
||||
}
|
||||
bytes += self.read_block(false)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl <R: BlockRead> Read for BlockedReader<R> {
|
||||
|
||||
fn read(&mut self, buffer: &mut [u8]) -> Result<usize, std::io::Error> {
|
||||
|
||||
if self.read_error {
|
||||
proxmox::io_bail!("detected read after error - internal error");
|
||||
}
|
||||
|
||||
let mut buffer_size = self.buffer.size();
|
||||
let mut rest = (buffer_size as isize) - (self.read_pos as isize);
|
||||
|
||||
if rest <= 0 && !self.got_eod { // try to refill buffer
|
||||
buffer_size = match self.read_block(true) {
|
||||
Ok(len) => len,
|
||||
err => {
|
||||
self.read_error = true;
|
||||
return err;
|
||||
}
|
||||
};
|
||||
rest = buffer_size as isize;
|
||||
}
|
||||
|
||||
if rest <= 0 {
|
||||
Ok(0)
|
||||
} else {
|
||||
let copy_len = if (buffer.len() as isize) < rest {
|
||||
buffer.len()
|
||||
} else {
|
||||
rest as usize
|
||||
};
|
||||
buffer[..copy_len].copy_from_slice(
|
||||
&self.buffer.payload[self.read_pos..(self.read_pos + copy_len)]);
|
||||
self.read_pos += copy_len;
|
||||
Ok(copy_len)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::io::Read;
|
||||
use anyhow::{bail, Error};
|
||||
use crate::{
|
||||
TapeWrite,
|
||||
BlockReadError,
|
||||
EmulateTapeReader,
|
||||
EmulateTapeWriter,
|
||||
PROXMOX_TAPE_BLOCK_SIZE,
|
||||
BlockedReader,
|
||||
BlockedWriter,
|
||||
};
|
||||
|
||||
fn write_and_verify(data: &[u8]) -> Result<(), Error> {
|
||||
|
||||
let mut tape_data = Vec::new();
|
||||
|
||||
{
|
||||
let writer = EmulateTapeWriter::new(&mut tape_data, 1024*1024*10);
|
||||
let mut writer = BlockedWriter::new(writer);
|
||||
|
||||
writer.write_all(data)?;
|
||||
|
||||
writer.finish(false)?;
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
tape_data.len(),
|
||||
((data.len() + PROXMOX_TAPE_BLOCK_SIZE)/PROXMOX_TAPE_BLOCK_SIZE)
|
||||
*PROXMOX_TAPE_BLOCK_SIZE
|
||||
);
|
||||
|
||||
let reader = &mut &tape_data[..];
|
||||
let reader = EmulateTapeReader::new(reader);
|
||||
let mut reader = BlockedReader::open(reader)?;
|
||||
|
||||
let mut read_data = Vec::with_capacity(PROXMOX_TAPE_BLOCK_SIZE);
|
||||
reader.read_to_end(&mut read_data)?;
|
||||
|
||||
assert_eq!(data.len(), read_data.len());
|
||||
|
||||
assert_eq!(data, &read_data[..]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_stream() -> Result<(), Error> {
|
||||
write_and_verify(b"")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_data() -> Result<(), Error> {
|
||||
write_and_verify(b"ABC")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_data() -> Result<(), Error> {
|
||||
let data = proxmox::sys::linux::random_data(1024*1024*5)?;
|
||||
write_and_verify(&data)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_data() -> Result<(), Error> {
|
||||
let tape_data = Vec::new();
|
||||
let reader = &mut &tape_data[..];
|
||||
let reader = EmulateTapeReader::new(reader);
|
||||
match BlockedReader::open(reader) {
|
||||
Err(BlockReadError::EndOfFile) => { /* OK */ },
|
||||
_ => bail!("expected EOF"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_end_marker() -> Result<(), Error> {
|
||||
let mut tape_data = Vec::new();
|
||||
{
|
||||
let writer = EmulateTapeWriter::new(&mut tape_data, 1024*1024);
|
||||
let mut writer = BlockedWriter::new(writer);
|
||||
// write at least one block
|
||||
let data = proxmox::sys::linux::random_data(PROXMOX_TAPE_BLOCK_SIZE)?;
|
||||
writer.write_all(&data)?;
|
||||
// but do not call finish here
|
||||
}
|
||||
|
||||
let reader = &mut &tape_data[..];
|
||||
let reader = EmulateTapeReader::new(reader);
|
||||
let mut reader = BlockedReader::open(reader)?;
|
||||
|
||||
let mut data = Vec::with_capacity(PROXMOX_TAPE_BLOCK_SIZE);
|
||||
assert!(reader.read_to_end(&mut data).is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_read_buffer() -> Result<(), Error> {
|
||||
let mut tape_data = Vec::new();
|
||||
|
||||
{
|
||||
let writer = EmulateTapeWriter::new(&mut tape_data, 1024*1024);
|
||||
let mut writer = BlockedWriter::new(writer);
|
||||
|
||||
writer.write_all(b"ABC")?;
|
||||
|
||||
writer.finish(false)?;
|
||||
}
|
||||
|
||||
let reader = &mut &tape_data[..];
|
||||
let reader = EmulateTapeReader::new(reader);
|
||||
let mut reader = BlockedReader::open(reader)?;
|
||||
|
||||
let mut buf = [0u8; 1];
|
||||
assert_eq!(reader.read(&mut buf)?, 1, "wrong byte count");
|
||||
assert_eq!(&buf, b"A");
|
||||
assert_eq!(reader.read(&mut buf)?, 1, "wrong byte count");
|
||||
assert_eq!(&buf, b"B");
|
||||
assert_eq!(reader.read(&mut buf)?, 1, "wrong byte count");
|
||||
assert_eq!(&buf, b"C");
|
||||
assert_eq!(reader.read(&mut buf)?, 0, "wrong byte count");
|
||||
assert_eq!(reader.read(&mut buf)?, 0, "wrong byte count");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
143
pbs-tape/src/blocked_writer.rs
Normal file
143
pbs-tape/src/blocked_writer.rs
Normal file
@ -0,0 +1,143 @@
|
||||
use proxmox::tools::vec;
|
||||
|
||||
use crate::{
|
||||
TapeWrite,
|
||||
BlockWrite,
|
||||
BlockHeader,
|
||||
BlockHeaderFlags,
|
||||
};
|
||||
|
||||
/// Assemble and write blocks of data
|
||||
///
|
||||
/// This type implement 'TapeWrite'. Data written is assembled to
|
||||
/// equally sized blocks (see 'BlockHeader'), which are then written
|
||||
/// to the underlying writer.
|
||||
pub struct BlockedWriter<W: BlockWrite> {
|
||||
writer: W,
|
||||
buffer: Box<BlockHeader>,
|
||||
buffer_pos: usize,
|
||||
seq_nr: u32,
|
||||
logical_end_of_media: bool,
|
||||
bytes_written: usize,
|
||||
wrote_eof: bool,
|
||||
}
|
||||
|
||||
impl <W: BlockWrite> Drop for BlockedWriter<W> {
|
||||
|
||||
// Try to make sure to end the file with a filemark
|
||||
fn drop(&mut self) {
|
||||
if !self.wrote_eof {
|
||||
let _ = self.writer.write_filemark();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl <W: BlockWrite> BlockedWriter<W> {
|
||||
|
||||
/// Allow access to underlying writer
|
||||
pub fn writer_ref_mut(&mut self) -> &mut W {
|
||||
&mut self.writer
|
||||
}
|
||||
|
||||
/// Creates a new instance.
|
||||
pub fn new(writer: W) -> Self {
|
||||
Self {
|
||||
writer,
|
||||
buffer: BlockHeader::new(),
|
||||
buffer_pos: 0,
|
||||
seq_nr: 0,
|
||||
logical_end_of_media: false,
|
||||
bytes_written: 0,
|
||||
wrote_eof: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_block(buffer: &BlockHeader, writer: &mut W) -> Result<bool, std::io::Error> {
|
||||
|
||||
let data = unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
(buffer as *const BlockHeader) as *const u8,
|
||||
BlockHeader::SIZE,
|
||||
)
|
||||
};
|
||||
writer.write_block(data)
|
||||
}
|
||||
|
||||
fn write_eof(&mut self) -> Result<(), std::io::Error> {
|
||||
if self.wrote_eof {
|
||||
proxmox::io_bail!("BlockedWriter: detected multiple EOF writes");
|
||||
}
|
||||
self.wrote_eof = true;
|
||||
|
||||
self.writer.write_filemark()
|
||||
}
|
||||
|
||||
fn write(&mut self, data: &[u8]) -> Result<usize, std::io::Error> {
|
||||
|
||||
if data.is_empty() { return Ok(0); }
|
||||
|
||||
let rest = self.buffer.payload.len() - self.buffer_pos;
|
||||
let bytes = if data.len() < rest { data.len() } else { rest };
|
||||
self.buffer.payload[self.buffer_pos..(self.buffer_pos+bytes)]
|
||||
.copy_from_slice(&data[..bytes]);
|
||||
|
||||
let rest = rest - bytes;
|
||||
|
||||
if rest == 0 {
|
||||
self.buffer.flags = BlockHeaderFlags::empty();
|
||||
self.buffer.set_size(self.buffer.payload.len());
|
||||
self.buffer.set_seq_nr(self.seq_nr);
|
||||
self.seq_nr += 1;
|
||||
let leom = Self::write_block(&self.buffer, &mut self.writer)?;
|
||||
if leom { self.logical_end_of_media = true; }
|
||||
self.buffer_pos = 0;
|
||||
self.bytes_written += BlockHeader::SIZE;
|
||||
|
||||
} else {
|
||||
self.buffer_pos += bytes;
|
||||
}
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl <W: BlockWrite> TapeWrite for BlockedWriter<W> {
|
||||
|
||||
fn write_all(&mut self, mut data: &[u8]) -> Result<bool, std::io::Error> {
|
||||
while !data.is_empty() {
|
||||
match self.write(data) {
|
||||
Ok(n) => data = &data[n..],
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Ok(self.logical_end_of_media)
|
||||
}
|
||||
|
||||
fn bytes_written(&self) -> usize {
|
||||
self.bytes_written
|
||||
}
|
||||
|
||||
/// flush last block, set END_OF_STREAM flag
|
||||
///
|
||||
/// Note: This may write an empty block just including the
|
||||
/// END_OF_STREAM flag.
|
||||
fn finish(&mut self, incomplete: bool) -> Result<bool, std::io::Error> {
|
||||
vec::clear(&mut self.buffer.payload[self.buffer_pos..]);
|
||||
self.buffer.flags = BlockHeaderFlags::END_OF_STREAM;
|
||||
if incomplete { self.buffer.flags |= BlockHeaderFlags::INCOMPLETE; }
|
||||
self.buffer.set_size(self.buffer_pos);
|
||||
self.buffer.set_seq_nr(self.seq_nr);
|
||||
self.seq_nr += 1;
|
||||
self.bytes_written += BlockHeader::SIZE;
|
||||
let leom = Self::write_block(&self.buffer, &mut self.writer)?;
|
||||
self.write_eof()?;
|
||||
Ok(leom)
|
||||
}
|
||||
|
||||
/// Returns if the writer already detected the logical end of media
|
||||
fn logical_end_of_media(&self) -> bool {
|
||||
self.logical_end_of_media
|
||||
}
|
||||
|
||||
}
|
47
pbs-tape/src/emulate_tape_reader.rs
Normal file
47
pbs-tape/src/emulate_tape_reader.rs
Normal file
@ -0,0 +1,47 @@
|
||||
use std::io::Read;
|
||||
|
||||
use proxmox::tools::io::ReadExt;
|
||||
|
||||
use crate::{BlockRead, BlockReadError, PROXMOX_TAPE_BLOCK_SIZE};
|
||||
|
||||
/// Emulate tape read behavior on a normal Reader
|
||||
///
|
||||
/// Tapes reads are always return one whole block PROXMOX_TAPE_BLOCK_SIZE.
|
||||
pub struct EmulateTapeReader<R: Read> {
|
||||
reader: R,
|
||||
got_eof: bool,
|
||||
}
|
||||
|
||||
impl <R: Read> EmulateTapeReader<R> {
|
||||
|
||||
pub fn new(reader: R) -> Self {
|
||||
Self { reader, got_eof: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl <R: Read> BlockRead for EmulateTapeReader<R> {
|
||||
fn read_block(&mut self, buffer: &mut [u8]) -> Result<usize, BlockReadError> {
|
||||
if self.got_eof {
|
||||
return Err(BlockReadError::Error(proxmox::io_format_err!("detected read after EOF!")));
|
||||
}
|
||||
match self.reader.read_exact_or_eof(buffer)? {
|
||||
false => {
|
||||
self.got_eof = true;
|
||||
Err(BlockReadError::EndOfFile)
|
||||
}
|
||||
true => {
|
||||
// test buffer len after EOF test (to allow EOF test with small buffers in BufferedReader)
|
||||
if buffer.len() != PROXMOX_TAPE_BLOCK_SIZE {
|
||||
return Err(BlockReadError::Error(
|
||||
proxmox::io_format_err!(
|
||||
"EmulateTapeReader: read_block with wrong block size ({} != {})",
|
||||
buffer.len(),
|
||||
PROXMOX_TAPE_BLOCK_SIZE,
|
||||
)
|
||||
));
|
||||
}
|
||||
Ok(buffer.len())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
68
pbs-tape/src/emulate_tape_writer.rs
Normal file
68
pbs-tape/src/emulate_tape_writer.rs
Normal file
@ -0,0 +1,68 @@
|
||||
use std::io::{self, Write};
|
||||
|
||||
use crate::{BlockWrite, PROXMOX_TAPE_BLOCK_SIZE};
|
||||
|
||||
/// Emulate tape write behavior on a normal Writer
|
||||
///
|
||||
/// Data need to be written in blocks of size PROXMOX_TAPE_BLOCK_SIZE.
|
||||
/// Before reaching the EOT, the writer returns ENOSPC (like a linux
|
||||
/// tape device).
|
||||
pub struct EmulateTapeWriter<W> {
|
||||
block_nr: usize,
|
||||
max_blocks: usize,
|
||||
writer: W,
|
||||
wrote_eof: bool,
|
||||
}
|
||||
|
||||
impl <W: Write> EmulateTapeWriter<W> {
|
||||
|
||||
/// Create a new instance allowing to write about max_size bytes
|
||||
pub fn new(writer: W, max_size: usize) -> Self {
|
||||
|
||||
let mut max_blocks = max_size/PROXMOX_TAPE_BLOCK_SIZE;
|
||||
|
||||
if max_blocks < 2 {
|
||||
max_blocks = 2; // at least 2 blocks
|
||||
}
|
||||
|
||||
Self {
|
||||
block_nr: 0,
|
||||
wrote_eof: false,
|
||||
writer,
|
||||
max_blocks,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl <W: Write> BlockWrite for EmulateTapeWriter<W> {
|
||||
|
||||
fn write_block(&mut self, buffer: &[u8]) -> Result<bool, io::Error> {
|
||||
|
||||
if buffer.len() != PROXMOX_TAPE_BLOCK_SIZE {
|
||||
proxmox::io_bail!("EmulateTapeWriter: got write with wrong block size ({} != {}",
|
||||
buffer.len(), PROXMOX_TAPE_BLOCK_SIZE);
|
||||
}
|
||||
|
||||
if self.block_nr >= self.max_blocks + 2 {
|
||||
return Err(io::Error::from_raw_os_error(nix::errno::Errno::ENOSPC as i32));
|
||||
}
|
||||
|
||||
self.writer.write_all(buffer)?;
|
||||
self.block_nr += 1;
|
||||
|
||||
if self.block_nr > self.max_blocks {
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn write_filemark(&mut self) -> Result<(), std::io::Error> {
|
||||
if self.wrote_eof {
|
||||
proxmox::io_bail!("EmulateTapeWriter: detected multiple EOF writes");
|
||||
}
|
||||
// do nothing, just record the call
|
||||
self.wrote_eof = true;
|
||||
Ok(())
|
||||
}
|
||||
}
|
334
pbs-tape/src/lib.rs
Normal file
334
pbs-tape/src/lib.rs
Normal file
@ -0,0 +1,334 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use bitflags::bitflags;
|
||||
use endian_trait::Endian;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use proxmox::tools::Uuid;
|
||||
use proxmox::api::schema::parse_property_string;
|
||||
|
||||
use pbs_api_types::{ScsiTapeChanger, SLOT_ARRAY_SCHEMA};
|
||||
|
||||
pub mod linux_list_drives;
|
||||
|
||||
pub mod sgutils2;
|
||||
|
||||
mod blocked_reader;
|
||||
pub use blocked_reader::BlockedReader;
|
||||
|
||||
mod blocked_writer;
|
||||
pub use blocked_writer::BlockedWriter;
|
||||
|
||||
mod tape_write;
|
||||
pub use tape_write::*;
|
||||
|
||||
mod tape_read;
|
||||
pub use tape_read::*;
|
||||
|
||||
mod emulate_tape_reader;
|
||||
pub use emulate_tape_reader::EmulateTapeReader;
|
||||
|
||||
mod emulate_tape_writer;
|
||||
pub use emulate_tape_writer::EmulateTapeWriter;
|
||||
|
||||
pub mod sg_tape;
|
||||
|
||||
pub mod sg_pt_changer;
|
||||
|
||||
/// We use 256KB blocksize (always)
|
||||
pub const PROXMOX_TAPE_BLOCK_SIZE: usize = 256*1024;
|
||||
|
||||
// openssl::sha::sha256(b"Proxmox Tape Block Header v1.0")[0..8]
|
||||
pub const PROXMOX_TAPE_BLOCK_HEADER_MAGIC_1_0: [u8; 8] = [220, 189, 175, 202, 235, 160, 165, 40];
|
||||
|
||||
// openssl::sha::sha256(b"Proxmox Backup Content Header v1.0")[0..8];
|
||||
pub const PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0: [u8; 8] = [99, 238, 20, 159, 205, 242, 155, 12];
|
||||
// openssl::sha::sha256(b"Proxmox Backup Tape Label v1.0")[0..8];
|
||||
pub const PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0: [u8; 8] = [42, 5, 191, 60, 176, 48, 170, 57];
|
||||
// openssl::sha::sha256(b"Proxmox Backup MediaSet Label v1.0")
|
||||
pub const PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0: [u8; 8] = [8, 96, 99, 249, 47, 151, 83, 216];
|
||||
|
||||
/// Tape Block Header with data payload
|
||||
///
|
||||
/// All tape files are written as sequence of blocks.
|
||||
///
|
||||
/// Note: this struct is large, never put this on the stack!
|
||||
/// so we use an unsized type to avoid that.
|
||||
///
|
||||
/// Tape data block are always read/written with a fixed size
|
||||
/// (`PROXMOX_TAPE_BLOCK_SIZE`). But they may contain less data, so the
|
||||
/// header has an additional size field. For streams of blocks, there
|
||||
/// is a sequence number (`seq_nr`) which may be use for additional
|
||||
/// error checking.
|
||||
#[repr(C,packed)]
|
||||
pub struct BlockHeader {
|
||||
/// fixed value `PROXMOX_TAPE_BLOCK_HEADER_MAGIC_1_0`
|
||||
pub magic: [u8; 8],
|
||||
pub flags: BlockHeaderFlags,
|
||||
/// size as 3 bytes unsigned, little endian
|
||||
pub size: [u8; 3],
|
||||
/// block sequence number
|
||||
pub seq_nr: u32,
|
||||
pub payload: [u8],
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Header flags (e.g. `END_OF_STREAM` or `INCOMPLETE`)
|
||||
pub struct BlockHeaderFlags: u8 {
|
||||
/// Marks the last block in a stream.
|
||||
const END_OF_STREAM = 0b00000001;
|
||||
/// Mark multivolume streams (when set in the last block)
|
||||
const INCOMPLETE = 0b00000010;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Endian, Copy, Clone, Debug)]
|
||||
#[repr(C,packed)]
|
||||
/// Media Content Header
|
||||
///
|
||||
/// All tape files start with this header. The header may contain some
|
||||
/// informational data indicated by `size`.
|
||||
///
|
||||
/// `| MediaContentHeader | header data (size) | stream data |`
|
||||
///
|
||||
/// Note: The stream data following may be of any size.
|
||||
pub struct MediaContentHeader {
|
||||
/// fixed value `PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0`
|
||||
pub magic: [u8; 8],
|
||||
/// magic number for the content following
|
||||
pub content_magic: [u8; 8],
|
||||
/// unique ID to identify this data stream
|
||||
pub uuid: [u8; 16],
|
||||
/// stream creation time
|
||||
pub ctime: i64,
|
||||
/// Size of header data
|
||||
pub size: u32,
|
||||
/// Part number for multipart archives.
|
||||
pub part_number: u8,
|
||||
/// Reserved for future use
|
||||
pub reserved_0: u8,
|
||||
/// Reserved for future use
|
||||
pub reserved_1: u8,
|
||||
/// Reserved for future use
|
||||
pub reserved_2: u8,
|
||||
}
|
||||
|
||||
impl MediaContentHeader {
|
||||
|
||||
/// Create a new instance with autogenerated Uuid
|
||||
pub fn new(content_magic: [u8; 8], size: u32) -> Self {
|
||||
let uuid = *proxmox::tools::uuid::Uuid::generate()
|
||||
.into_inner();
|
||||
Self {
|
||||
magic: PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0,
|
||||
content_magic,
|
||||
uuid,
|
||||
ctime: proxmox::tools::time::epoch_i64(),
|
||||
size,
|
||||
part_number: 0,
|
||||
reserved_0: 0,
|
||||
reserved_1: 0,
|
||||
reserved_2: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to check magic numbers and size constraints
|
||||
pub fn check(&self, content_magic: [u8; 8], min_size: u32, max_size: u32) -> Result<(), Error> {
|
||||
if self.magic != PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0 {
|
||||
bail!("MediaContentHeader: wrong magic");
|
||||
}
|
||||
if self.content_magic != content_magic {
|
||||
bail!("MediaContentHeader: wrong content magic");
|
||||
}
|
||||
if self.size < min_size || self.size > max_size {
|
||||
bail!("MediaContentHeader: got unexpected size");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the content Uuid
|
||||
pub fn content_uuid(&self) -> Uuid {
|
||||
Uuid::from(self.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl BlockHeader {
|
||||
|
||||
pub const SIZE: usize = PROXMOX_TAPE_BLOCK_SIZE;
|
||||
|
||||
/// Allocates a new instance on the heap
|
||||
pub fn new() -> Box<Self> {
|
||||
use std::alloc::{alloc_zeroed, Layout};
|
||||
|
||||
// align to PAGESIZE, so that we can use it with SG_IO
|
||||
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
|
||||
|
||||
let mut buffer = unsafe {
|
||||
let ptr = alloc_zeroed(
|
||||
Layout::from_size_align(Self::SIZE, page_size)
|
||||
.unwrap(),
|
||||
);
|
||||
Box::from_raw(
|
||||
std::slice::from_raw_parts_mut(ptr, Self::SIZE - 16)
|
||||
as *mut [u8] as *mut Self
|
||||
)
|
||||
};
|
||||
buffer.magic = PROXMOX_TAPE_BLOCK_HEADER_MAGIC_1_0;
|
||||
buffer
|
||||
}
|
||||
|
||||
/// Set the `size` field
|
||||
pub fn set_size(&mut self, size: usize) {
|
||||
let size = size.to_le_bytes();
|
||||
self.size.copy_from_slice(&size[..3]);
|
||||
}
|
||||
|
||||
/// Returns the `size` field
|
||||
pub fn size(&self) -> usize {
|
||||
(self.size[0] as usize) + ((self.size[1] as usize)<<8) + ((self.size[2] as usize)<<16)
|
||||
}
|
||||
|
||||
/// Set the `seq_nr` field
|
||||
pub fn set_seq_nr(&mut self, seq_nr: u32) {
|
||||
self.seq_nr = seq_nr.to_le();
|
||||
}
|
||||
|
||||
/// Returns the `seq_nr` field
|
||||
pub fn seq_nr(&self) -> u32 {
|
||||
u32::from_le(self.seq_nr)
|
||||
}
|
||||
}
|
||||
|
||||
/// Changer element status.
|
||||
///
|
||||
/// Drive and slots may be `Empty`, or contain some media, either
|
||||
/// with known volume tag `VolumeTag(String)`, or without (`Full`).
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum ElementStatus {
|
||||
Empty,
|
||||
Full,
|
||||
VolumeTag(String),
|
||||
}
|
||||
|
||||
/// Changer drive status.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
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>,
|
||||
/// Drive Vendor
|
||||
pub vendor: Option<String>,
|
||||
/// Drive Model
|
||||
pub model: Option<String>,
|
||||
/// Element Address
|
||||
pub element_address: u16,
|
||||
}
|
||||
|
||||
/// Storage element status.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
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.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TransportElementStatus {
|
||||
/// The status.
|
||||
pub status: ElementStatus,
|
||||
/// Element Address
|
||||
pub element_address: u16,
|
||||
}
|
||||
|
||||
/// Changer status - show drive/slot usage
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MtxStatus {
|
||||
/// List of known drives
|
||||
pub drives: Vec<DriveStatus>,
|
||||
/// List of known storage slots
|
||||
pub slots: Vec<StorageElementStatus>,
|
||||
/// Transport 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)
|
||||
}
|
||||
|
||||
pub fn find_free_slot(&self, import_export: bool) -> Option<u64> {
|
||||
let mut free_slot = None;
|
||||
for (i, slot_info) in self.slots.iter().enumerate() {
|
||||
if slot_info.import_export != import_export {
|
||||
continue; // skip slots of wrong type
|
||||
}
|
||||
if let ElementStatus::Empty = slot_info.status {
|
||||
free_slot = Some((i+1) as u64);
|
||||
break;
|
||||
}
|
||||
}
|
||||
free_slot
|
||||
}
|
||||
|
||||
pub fn mark_import_export_slots(&mut self, config: &ScsiTapeChanger) -> Result<(), Error>{
|
||||
let mut export_slots: HashSet<u64> = HashSet::new();
|
||||
|
||||
if let Some(slots) = &config.export_slots {
|
||||
let slots: Value = parse_property_string(&slots, &SLOT_ARRAY_SCHEMA)?;
|
||||
export_slots = slots
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter_map(|v| v.as_u64())
|
||||
.collect();
|
||||
}
|
||||
|
||||
for (i, entry) in self.slots.iter_mut().enumerate() {
|
||||
let slot = i as u64 + 1;
|
||||
if export_slots.contains(&slot) {
|
||||
entry.import_export = true; // mark as IMPORT/EXPORT
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
322
pbs-tape/src/linux_list_drives.rs
Normal file
322
pbs-tape/src/linux_list_drives.rs
Normal file
@ -0,0 +1,322 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{OpenOptions, File};
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use nix::fcntl::{fcntl, FcntlArg, OFlag};
|
||||
|
||||
use proxmox::sys::error::SysResult;
|
||||
|
||||
use pbs_tools::fs::scan_subdir;
|
||||
use pbs_api_types::{DeviceKind, OptionalDeviceIdentification, TapeDeviceInfo};
|
||||
|
||||
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> {
|
||||
|
||||
let mut list = Vec::new();
|
||||
|
||||
let dir_iter = match scan_subdir(
|
||||
libc::AT_FDCWD,
|
||||
"/sys/class/scsi_generic",
|
||||
&SCSI_GENERIC_NAME_REGEX)
|
||||
{
|
||||
Err(_) => return list,
|
||||
Ok(iter) => iter,
|
||||
};
|
||||
|
||||
for item in dir_iter {
|
||||
let item = match item {
|
||||
Err(_) => continue,
|
||||
Ok(item) => item,
|
||||
};
|
||||
|
||||
let name = item.file_name().to_str().unwrap().to_string();
|
||||
|
||||
let mut sys_path = PathBuf::from("/sys/class/scsi_generic");
|
||||
sys_path.push(&name);
|
||||
|
||||
let device = match udev::Device::from_syspath(&sys_path) {
|
||||
Err(_) => continue,
|
||||
Ok(device) => device,
|
||||
};
|
||||
|
||||
let devnum = match device.devnum() {
|
||||
None => continue,
|
||||
Some(devnum) => devnum,
|
||||
};
|
||||
|
||||
let parent = match device.parent() {
|
||||
None => continue,
|
||||
Some(parent) => parent,
|
||||
};
|
||||
|
||||
match parent.attribute_value("type") {
|
||||
Some(type_osstr) => {
|
||||
if type_osstr != "8" {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => { continue; }
|
||||
}
|
||||
|
||||
// let mut test_path = sys_path.clone();
|
||||
// test_path.push("device/scsi_changer");
|
||||
// if !test_path.exists() { continue; }
|
||||
|
||||
let _dev_path = match device.devnode().map(Path::to_owned) {
|
||||
None => continue,
|
||||
Some(dev_path) => dev_path,
|
||||
};
|
||||
|
||||
let serial = match device.property_value("ID_SCSI_SERIAL")
|
||||
.map(std::ffi::OsString::from)
|
||||
.and_then(|s| if let Ok(s) = s.into_string() { Some(s) } else { None })
|
||||
{
|
||||
None => continue,
|
||||
Some(serial) => serial,
|
||||
};
|
||||
|
||||
let vendor = device.property_value("ID_VENDOR")
|
||||
.map(std::ffi::OsString::from)
|
||||
.and_then(|s| if let Ok(s) = s.into_string() { Some(s) } else { None })
|
||||
.unwrap_or_else(|| String::from("unknown"));
|
||||
|
||||
let model = device.property_value("ID_MODEL")
|
||||
.map(std::ffi::OsString::from)
|
||||
.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-{}", serial);
|
||||
|
||||
if PathBuf::from(&dev_path).exists() {
|
||||
list.push(TapeDeviceInfo {
|
||||
kind: DeviceKind::Changer,
|
||||
path: dev_path,
|
||||
serial,
|
||||
vendor,
|
||||
model,
|
||||
major: unsafe { libc::major(devnum) },
|
||||
minor: unsafe { libc::minor(devnum) },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
list
|
||||
}
|
||||
|
||||
/// 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_generic",
|
||||
&SCSI_GENERIC_NAME_REGEX)
|
||||
{
|
||||
Err(_) => return list,
|
||||
Ok(iter) => iter,
|
||||
};
|
||||
|
||||
for item in dir_iter {
|
||||
let item = match item {
|
||||
Err(_) => continue,
|
||||
Ok(item) => item,
|
||||
};
|
||||
|
||||
let name = item.file_name().to_str().unwrap().to_string();
|
||||
|
||||
let mut sys_path = PathBuf::from("/sys/class/scsi_generic");
|
||||
sys_path.push(&name);
|
||||
|
||||
let device = match udev::Device::from_syspath(&sys_path) {
|
||||
Err(_) => continue,
|
||||
Ok(device) => device,
|
||||
};
|
||||
|
||||
let devnum = match device.devnum() {
|
||||
None => continue,
|
||||
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,
|
||||
};
|
||||
|
||||
let serial = match device.property_value("ID_SCSI_SERIAL")
|
||||
.map(std::ffi::OsString::from)
|
||||
.and_then(|s| if let Ok(s) = s.into_string() { Some(s) } else { None })
|
||||
{
|
||||
None => continue,
|
||||
Some(serial) => serial,
|
||||
};
|
||||
|
||||
let vendor = device.property_value("ID_VENDOR")
|
||||
.map(std::ffi::OsString::from)
|
||||
.and_then(|s| if let Ok(s) = s.into_string() { Some(s) } else { None })
|
||||
.unwrap_or_else(|| String::from("unknown"));
|
||||
|
||||
let model = device.property_value("ID_MODEL")
|
||||
.map(std::ffi::OsString::from)
|
||||
.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-{}-sg", serial);
|
||||
|
||||
if PathBuf::from(&dev_path).exists() {
|
||||
list.push(TapeDeviceInfo {
|
||||
kind: DeviceKind::Tape,
|
||||
path: dev_path,
|
||||
serial,
|
||||
vendor,
|
||||
model,
|
||||
major: unsafe { libc::major(devnum) },
|
||||
minor: unsafe { libc::minor(devnum) },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
list
|
||||
}
|
||||
|
||||
/// Test if a device exists, and returns associated `TapeDeviceInfo`
|
||||
pub fn lookup_device<'a>(
|
||||
devices: &'a[TapeDeviceInfo],
|
||||
path: &str,
|
||||
) -> Option<&'a TapeDeviceInfo> {
|
||||
|
||||
if let Ok(stat) = nix::sys::stat::stat(path) {
|
||||
|
||||
let major = unsafe { libc::major(stat.st_rdev) };
|
||||
let minor = unsafe { libc::minor(stat.st_rdev) };
|
||||
|
||||
devices.iter().find(|d| d.major == major && d.minor == minor)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Lookup optional drive identification attributes
|
||||
pub fn lookup_device_identification<'a>(
|
||||
devices: &'a[TapeDeviceInfo],
|
||||
path: &str,
|
||||
) -> OptionalDeviceIdentification {
|
||||
|
||||
if let Some(info) = lookup_device(devices, path) {
|
||||
OptionalDeviceIdentification {
|
||||
vendor: Some(info.vendor.clone()),
|
||||
model: Some(info.model.clone()),
|
||||
serial: Some(info.serial.clone()),
|
||||
}
|
||||
} else {
|
||||
OptionalDeviceIdentification {
|
||||
vendor: None,
|
||||
model: None,
|
||||
serial: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 lto SCSI-generic tape device", path);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
|
||||
// shell completion helper
|
||||
|
||||
/// List changer device paths
|
||||
pub fn complete_changer_path(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
|
||||
linux_tape_changer_list().iter().map(|v| v.path.clone()).collect()
|
||||
}
|
||||
|
||||
/// List tape device paths
|
||||
pub fn complete_drive_path(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
|
||||
lto_tape_device_list().iter().map(|v| v.path.clone()).collect()
|
||||
}
|
950
pbs-tape/src/sg_pt_changer.rs
Normal file
950
pbs-tape/src/sg_pt_changer.rs
Normal file
@ -0,0 +1,950 @@
|
||||
//! 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 pbs_api_types::ScsiTapeChanger;
|
||||
|
||||
use crate::{
|
||||
ElementStatus,MtxStatus,TransportElementStatus,DriveStatus,StorageElementStatus,
|
||||
sgutils2::{
|
||||
SgRaw,
|
||||
SENSE_KEY_NOT_READY,
|
||||
ScsiError,
|
||||
scsi_ascii_to_string,
|
||||
scsi_inquiry,
|
||||
},
|
||||
};
|
||||
|
||||
const SCSI_CHANGER_DEFAULT_TIMEOUT: usize = 60*5; // 5 minutes
|
||||
const SCSI_VOLUME_TAG_LEN: usize = 36;
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// Execute scsi commands, optionally repeat the command until
|
||||
/// successful or timeout (sleep 1 second between invovations)
|
||||
///
|
||||
/// Timeout is 5 seconds. If the device reports "Not Ready - becoming
|
||||
/// ready", we wait up to 5 minutes.
|
||||
///
|
||||
/// Skipped errors are printed on stderr.
|
||||
fn execute_scsi_command<F: AsRawFd>(
|
||||
sg_raw: &mut SgRaw<F>,
|
||||
cmd: &[u8],
|
||||
error_prefix: &str,
|
||||
retry: bool,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
|
||||
let start = std::time::SystemTime::now();
|
||||
|
||||
let mut last_msg: Option<String> = None;
|
||||
|
||||
let mut timeout = std::time::Duration::new(5, 0); // short timeout by default
|
||||
|
||||
loop {
|
||||
match sg_raw.do_command(&cmd) {
|
||||
Ok(data) => return Ok(data.to_vec()),
|
||||
Err(err) if !retry => bail!("{} failed: {}", error_prefix, err),
|
||||
Err(err) => {
|
||||
let msg = err.to_string();
|
||||
if let Some(ref last) = last_msg {
|
||||
if &msg != last {
|
||||
eprintln!("{}", err);
|
||||
last_msg = Some(msg);
|
||||
}
|
||||
} else {
|
||||
eprintln!("{}", err);
|
||||
last_msg = Some(msg);
|
||||
}
|
||||
|
||||
if let ScsiError::Sense(ref sense) = err {
|
||||
// Not Ready - becoming ready
|
||||
if sense.sense_key == SENSE_KEY_NOT_READY && sense.asc == 0x04 && sense.ascq == 1 {
|
||||
// wait up to 5 minutes, long enough to finish inventorize
|
||||
timeout = std::time::Duration::new(5*60, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if start.elapsed()? > timeout {
|
||||
bail!("{} failed: {}", error_prefix, err);
|
||||
}
|
||||
|
||||
std::thread::sleep(std::time::Duration::new(1, 0));
|
||||
continue; // try again
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 = execute_scsi_command(&mut sg_raw, &cmd, "read element address assignment", true)?;
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Transfer 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(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ElementType {
|
||||
MediumTransport,
|
||||
Storage,
|
||||
ImportExport,
|
||||
DataTransfer,
|
||||
DataTransferWithDVCID,
|
||||
}
|
||||
|
||||
impl ElementType {
|
||||
fn byte1(&self) -> u8 {
|
||||
let volume_tag_bit = 1u8 << 4;
|
||||
match *self {
|
||||
ElementType::MediumTransport => volume_tag_bit | 1,
|
||||
ElementType::Storage => volume_tag_bit | 2,
|
||||
ElementType::ImportExport => volume_tag_bit | 3,
|
||||
ElementType::DataTransfer => volume_tag_bit | 4,
|
||||
// some changers cannot get voltag + dvcid at the same time
|
||||
ElementType::DataTransferWithDVCID => 4,
|
||||
}
|
||||
}
|
||||
|
||||
fn byte6(&self) -> u8 {
|
||||
match *self {
|
||||
ElementType::DataTransferWithDVCID => 0b001, // Mixed=0,CurData=0,DVCID=1
|
||||
_ => 0b000, // Mixed=0,CurData=0,DVCID=0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scsi_read_element_status_cdb(
|
||||
start_element_address: u16,
|
||||
number_of_elements: u16,
|
||||
element_type: ElementType,
|
||||
allocation_len: u32,
|
||||
) -> Vec<u8> {
|
||||
|
||||
let mut cmd = Vec::new();
|
||||
cmd.push(0xB8); // READ ELEMENT STATUS (B8h)
|
||||
cmd.push(element_type.byte1());
|
||||
cmd.extend(&start_element_address.to_be_bytes());
|
||||
|
||||
cmd.extend(&number_of_elements.to_be_bytes());
|
||||
cmd.push(element_type.byte6());
|
||||
cmd.extend(&allocation_len.to_be_bytes()[1..4]);
|
||||
cmd.push(0);
|
||||
cmd.push(0);
|
||||
|
||||
cmd
|
||||
}
|
||||
|
||||
// query a single element type from the changer
|
||||
fn get_element<F: AsRawFd>(
|
||||
sg_raw: &mut SgRaw<F>,
|
||||
element_type: ElementType,
|
||||
allocation_len: u32,
|
||||
mut retry: bool,
|
||||
) -> Result<DecodedStatusPage, Error> {
|
||||
|
||||
let mut start_element_address = 0;
|
||||
let number_of_elements: u16 = 1000; // some changers limit the query
|
||||
|
||||
let mut result = DecodedStatusPage {
|
||||
last_element_address: None,
|
||||
transports: Vec::new(),
|
||||
drives: Vec::new(),
|
||||
storage_slots: Vec::new(),
|
||||
import_export_slots: Vec::new(),
|
||||
};
|
||||
|
||||
loop {
|
||||
let cmd = scsi_read_element_status_cdb(start_element_address, number_of_elements, element_type, allocation_len);
|
||||
|
||||
let data = execute_scsi_command(sg_raw, &cmd, "read element status (B8h)", retry)?;
|
||||
|
||||
let page = decode_element_status_page(&data, start_element_address)?;
|
||||
|
||||
retry = false; // only retry the first command
|
||||
|
||||
let returned_number_of_elements = page.transports.len()
|
||||
+ page.drives.len()
|
||||
+ page.storage_slots.len()
|
||||
+ page.import_export_slots.len();
|
||||
|
||||
result.transports.extend(page.transports);
|
||||
result.drives.extend(page.drives);
|
||||
result.storage_slots.extend(page.storage_slots);
|
||||
result.import_export_slots.extend(page.import_export_slots);
|
||||
result.last_element_address = page.last_element_address;
|
||||
|
||||
if let Some(last_element_address) = page.last_element_address {
|
||||
if last_element_address < start_element_address {
|
||||
bail!("got strange element address");
|
||||
}
|
||||
if returned_number_of_elements >= (number_of_elements as usize) {
|
||||
start_element_address = last_element_address + 1;
|
||||
continue; // we possibly have to read additional elements
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 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 drives = Vec::new();
|
||||
let mut storage_slots = Vec::new();
|
||||
let mut import_export_slots = Vec::new();
|
||||
let mut transports = Vec::new();
|
||||
|
||||
let page = get_element(&mut sg_raw, ElementType::Storage, allocation_len, true)?;
|
||||
storage_slots.extend(page.storage_slots);
|
||||
|
||||
let page = get_element(&mut sg_raw, ElementType::ImportExport, allocation_len, false)?;
|
||||
import_export_slots.extend(page.import_export_slots);
|
||||
|
||||
let page = get_element(&mut sg_raw, ElementType::DataTransfer, allocation_len, false)?;
|
||||
drives.extend(page.drives);
|
||||
|
||||
// get the serial + vendor + model,
|
||||
// some changer require this to be an extra scsi command
|
||||
let page = get_element(&mut sg_raw, ElementType::DataTransferWithDVCID, allocation_len, false)?;
|
||||
// should be in same order and same count, but be on the safe side.
|
||||
// there should not be too many drives normally
|
||||
for drive in drives.iter_mut() {
|
||||
for drive2 in &page.drives {
|
||||
if drive2.element_address == drive.element_address {
|
||||
drive.vendor = drive2.vendor.clone();
|
||||
drive.model = drive2.model.clone();
|
||||
drive.drive_serial_number = drive2.drive_serial_number.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let page = get_element(&mut sg_raw, ElementType::MediumTransport, allocation_len, false)?;
|
||||
transports.extend(page.transports);
|
||||
|
||||
let transport_count = setup.transport_element_count as usize;
|
||||
let storage_count = setup.storage_element_count as usize;
|
||||
let import_export_count = setup.import_export_element_count as usize;
|
||||
let transfer_count = setup.transfer_element_count as usize;
|
||||
|
||||
if transport_count != transports.len() {
|
||||
bail!(
|
||||
"got wrong number of transport elements: expoected {}, got{}",
|
||||
transport_count,
|
||||
transports.len()
|
||||
);
|
||||
}
|
||||
if storage_count != storage_slots.len() {
|
||||
bail!(
|
||||
"got wrong number of storage elements: expected {}, got {}",
|
||||
storage_count,
|
||||
storage_slots.len(),
|
||||
);
|
||||
}
|
||||
if import_export_count != import_export_slots.len() {
|
||||
bail!(
|
||||
"got wrong number of import/export elements: expected {}, got {}",
|
||||
import_export_count,
|
||||
import_export_slots.len(),
|
||||
);
|
||||
}
|
||||
if transfer_count != drives.len() {
|
||||
bail!(
|
||||
"got wrong number of transfer elements: expected {}, got {}",
|
||||
transfer_count,
|
||||
drives.len(),
|
||||
);
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
/// Read status and map import-export slots from config
|
||||
pub fn status(config: &ScsiTapeChanger) -> Result<MtxStatus, Error> {
|
||||
let path = &config.path;
|
||||
|
||||
let mut file = open(path)
|
||||
.map_err(|err| format_err!("error opening '{}': {}", path, err))?;
|
||||
let mut status = read_element_status(&mut file)
|
||||
.map_err(|err| format_err!("error reading element status: {}", err))?;
|
||||
|
||||
status.mark_import_export_slots(&config)?;
|
||||
|
||||
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,
|
||||
reserved: 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(SCSI_VOLUME_TAG_LEN)?;
|
||||
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(SCSI_VOLUME_TAG_LEN)?;
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Endian)]
|
||||
struct TransportDescriptor { // 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
|
||||
}
|
||||
}
|
||||
|
||||
struct DvcidInfo {
|
||||
vendor: Option<String>,
|
||||
model: Option<String>,
|
||||
serial: Option<String>,
|
||||
}
|
||||
|
||||
fn decode_dvcid_info<R: Read>(reader: &mut R) -> Result<DvcidInfo, Error> {
|
||||
let dvcid: DvcidHead = unsafe { reader.read_be_value()? };
|
||||
|
||||
let (serial, vendor, model) = 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), None, None)
|
||||
}
|
||||
(2, 1) => {
|
||||
if dvcid.identifier_len != 34 {
|
||||
bail!("got wrong DVCID length");
|
||||
}
|
||||
let vendor = reader.read_exact_allocated(8)?;
|
||||
let vendor = scsi_ascii_to_string(&vendor);
|
||||
let model = reader.read_exact_allocated(16)?;
|
||||
let model = scsi_ascii_to_string(&model);
|
||||
let serial = reader.read_exact_allocated(10)?;
|
||||
let serial = scsi_ascii_to_string(&serial);
|
||||
(Some(serial), Some(vendor), Some(model))
|
||||
}
|
||||
_ => (None, None, None),
|
||||
};
|
||||
|
||||
Ok(DvcidInfo {
|
||||
vendor,
|
||||
model,
|
||||
serial,
|
||||
})
|
||||
}
|
||||
|
||||
fn decode_element_status_page(
|
||||
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
|
||||
}
|
||||
|
||||
let len = head.byte_count_of_report_available;
|
||||
let len = ((len[0] as usize) << 16) + ((len[1] as usize) << 8) + (len[2] as usize);
|
||||
|
||||
if len < reader.len() {
|
||||
reader = &reader[..len];
|
||||
} else if len > reader.len() {
|
||||
bail!("wrong amount of data: expected {}, got {}", len, reader.len());
|
||||
}
|
||||
|
||||
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 descr_len = subhead.descriptor_length as usize;
|
||||
|
||||
if descr_len == 0 {
|
||||
bail!("got elements, but descriptor length 0");
|
||||
}
|
||||
|
||||
for descriptor in descr_data.chunks_exact(descr_len) {
|
||||
let mut reader = &descriptor[..];
|
||||
|
||||
match subhead.element_type_code {
|
||||
1 => {
|
||||
let desc: TransportDescriptor = 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)?;
|
||||
|
||||
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)?;
|
||||
|
||||
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 = decode_dvcid_info(&mut reader).unwrap_or(DvcidInfo {
|
||||
vendor: None,
|
||||
model: None,
|
||||
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: dvcid.serial,
|
||||
vendor: dvcid.vendor,
|
||||
model: dvcid.model,
|
||||
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)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use anyhow::Error;
|
||||
use super::*;
|
||||
|
||||
struct StorageDesc {
|
||||
address: u16,
|
||||
pvoltag: Option<String>,
|
||||
}
|
||||
|
||||
fn build_element_status_page(
|
||||
descriptors: Vec<StorageDesc>,
|
||||
trailing: &[u8],
|
||||
element_type: u8,
|
||||
) -> Vec<u8> {
|
||||
let descs: Vec<Vec<u8>> = descriptors.iter().map(|desc| {
|
||||
build_storage_descriptor(&desc, trailing)
|
||||
}).collect();
|
||||
|
||||
let (desc_len, address) = if let Some(el) = descs.get(0) {
|
||||
(el.len() as u16, descriptors[0].address)
|
||||
} else {
|
||||
(0u16, 0u16)
|
||||
};
|
||||
|
||||
let descriptor_byte_count = desc_len * descs.len() as u16;
|
||||
let byte_count = 8 + descriptor_byte_count;
|
||||
|
||||
let mut res = Vec::new();
|
||||
|
||||
res.extend_from_slice(&address.to_be_bytes());
|
||||
res.extend_from_slice(&(descs.len() as u16).to_be_bytes());
|
||||
res.push(0);
|
||||
let byte_count = byte_count as u32;
|
||||
res.extend_from_slice(&byte_count.to_be_bytes()[1..]);
|
||||
|
||||
res.push(element_type);
|
||||
res.push(0x80);
|
||||
res.extend_from_slice(&desc_len.to_be_bytes());
|
||||
res.push(0);
|
||||
let descriptor_byte_count = descriptor_byte_count as u32;
|
||||
res.extend_from_slice(&descriptor_byte_count.to_be_bytes()[1..]);
|
||||
|
||||
for desc in descs {
|
||||
res.extend_from_slice(&desc);
|
||||
}
|
||||
|
||||
res.extend_from_slice(trailing);
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn build_storage_descriptor(
|
||||
desc: &StorageDesc,
|
||||
trailing: &[u8],
|
||||
) -> Vec<u8> {
|
||||
let mut res = Vec::new();
|
||||
res.push(((desc.address >> 8) & 0xFF) as u8);
|
||||
res.push((desc.address & 0xFF) as u8);
|
||||
if desc.pvoltag.is_some() {
|
||||
res.push(0x01); // full
|
||||
} else {
|
||||
res.push(0x00); // full
|
||||
}
|
||||
|
||||
res.extend_from_slice(&[0,0,0,0,0,0,0x80]);
|
||||
res.push(((desc.address >> 8) & 0xFF) as u8);
|
||||
res.push((desc.address & 0xFF) as u8);
|
||||
|
||||
if let Some(voltag) = &desc.pvoltag {
|
||||
res.extend_from_slice(voltag.as_bytes());
|
||||
let rem = SCSI_VOLUME_TAG_LEN - voltag.as_bytes().len();
|
||||
if rem > 0 {
|
||||
res.resize(res.len() + rem, 0);
|
||||
}
|
||||
}
|
||||
|
||||
res.extend_from_slice(trailing);
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_page_valid() -> Result<(), Error> {
|
||||
let descs = vec![
|
||||
StorageDesc {
|
||||
address: 0,
|
||||
pvoltag: Some("0123456789".to_string()),
|
||||
},
|
||||
StorageDesc {
|
||||
address: 1,
|
||||
pvoltag: Some("1234567890".to_string()),
|
||||
},
|
||||
];
|
||||
let test_data = build_element_status_page(descs, &[], 0x2);
|
||||
let page = decode_element_status_page(&test_data, 0)?;
|
||||
assert_eq!(page.storage_slots.len(), 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_page_too_short() -> Result<(), Error> {
|
||||
let descs = vec![
|
||||
StorageDesc {
|
||||
address: 0,
|
||||
pvoltag: Some("0123456789".to_string()),
|
||||
},
|
||||
StorageDesc {
|
||||
address: 1,
|
||||
pvoltag: Some("1234567890".to_string()),
|
||||
},
|
||||
];
|
||||
let test_data = build_element_status_page(descs, &[], 0x2);
|
||||
let len = test_data.len();
|
||||
let res = decode_element_status_page(&test_data[..(len - 10)], 0);
|
||||
assert!(res.is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_page_too_large() -> Result<(), Error> {
|
||||
let descs = vec![
|
||||
StorageDesc {
|
||||
address: 0,
|
||||
pvoltag: Some("0123456789".to_string()),
|
||||
},
|
||||
StorageDesc {
|
||||
address: 1,
|
||||
pvoltag: Some("1234567890".to_string()),
|
||||
},
|
||||
];
|
||||
let test_data = build_element_status_page(descs, &[0,0,0,0,0], 0x2);
|
||||
let page = decode_element_status_page(&test_data, 0)?;
|
||||
assert_eq!(page.storage_slots.len(), 2);
|
||||
Ok(())
|
||||
}
|
||||
}
|
990
pbs-tape/src/sg_tape.rs
Normal file
990
pbs-tape/src/sg_tape.rs
Normal file
@ -0,0 +1,990 @@
|
||||
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 std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use endian_trait::Endian;
|
||||
use nix::fcntl::{fcntl, FcntlArg, OFlag};
|
||||
|
||||
mod encryption;
|
||||
pub use encryption::*;
|
||||
|
||||
mod volume_statistics;
|
||||
pub use volume_statistics::*;
|
||||
|
||||
mod tape_alert_flags;
|
||||
pub use tape_alert_flags::*;
|
||||
|
||||
mod mam;
|
||||
pub use mam::*;
|
||||
|
||||
mod report_density;
|
||||
pub use report_density::*;
|
||||
|
||||
use proxmox::{
|
||||
sys::error::SysResult,
|
||||
tools::io::{ReadExt, WriteExt},
|
||||
};
|
||||
|
||||
use pbs_api_types::{MamAttribute, Lp17VolumeStatistics, LtoDriveAndMediaStatus};
|
||||
|
||||
use crate::{
|
||||
BlockRead,
|
||||
BlockReadError,
|
||||
BlockWrite,
|
||||
BlockedWriter,
|
||||
BlockedReader,
|
||||
sgutils2::{
|
||||
SgRaw,
|
||||
SenseInfo,
|
||||
ScsiError,
|
||||
InquiryInfo,
|
||||
ModeParameterHeader,
|
||||
ModeBlockDescriptor,
|
||||
alloc_page_aligned_buffer,
|
||||
scsi_inquiry,
|
||||
scsi_mode_sense,
|
||||
scsi_request_sense,
|
||||
},
|
||||
};
|
||||
|
||||
#[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],
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Endian, Debug, Copy, Clone)]
|
||||
struct DataCompressionModePage {
|
||||
page_code: u8, // 0x0f
|
||||
page_length: u8, // 0x0e
|
||||
flags2: u8,
|
||||
flags3: u8,
|
||||
compression_algorithm: u32,
|
||||
decompression_algorithm: u32,
|
||||
reserved: [u8;4],
|
||||
}
|
||||
|
||||
impl DataCompressionModePage {
|
||||
|
||||
pub fn set_compression(&mut self, enable: bool) {
|
||||
if enable {
|
||||
self.flags2 |= 128;
|
||||
} else {
|
||||
self.flags2 = self.flags2 & 127;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compression_enabled(&self) -> bool {
|
||||
(self.flags2 & 0b1000_0000) != 0
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Endian)]
|
||||
struct MediumConfigurationModePage {
|
||||
page_code: u8, // 0x1d
|
||||
page_length: u8, // 0x1e
|
||||
flags2: u8,
|
||||
reserved: [u8;29],
|
||||
}
|
||||
|
||||
impl MediumConfigurationModePage {
|
||||
|
||||
pub fn is_worm(&self) -> bool {
|
||||
(self.flags2 & 1) == 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LtoTapeStatus {
|
||||
pub block_length: u32,
|
||||
pub density_code: u8,
|
||||
pub buffer_mode: u8,
|
||||
pub write_protect: bool,
|
||||
pub compression: bool,
|
||||
}
|
||||
|
||||
pub struct SgTape {
|
||||
file: File,
|
||||
locate_offset: Option<i64>,
|
||||
info: InquiryInfo,
|
||||
encryption_key_loaded: bool,
|
||||
}
|
||||
|
||||
impl SgTape {
|
||||
|
||||
const SCSI_TAPE_DEFAULT_TIMEOUT: usize = 60*10; // 10 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,
|
||||
info,
|
||||
encryption_key_loaded: false,
|
||||
locate_offset: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Access to file descriptor - useful for testing
|
||||
pub fn file_mut(&mut self) -> &mut File {
|
||||
&mut self.file
|
||||
}
|
||||
|
||||
pub fn info(&self) -> &InquiryInfo {
|
||||
&self.info
|
||||
}
|
||||
|
||||
/// Return the maximum supported density code
|
||||
///
|
||||
/// This can be used to detect the drive generation.
|
||||
pub fn max_density_code(&mut self) -> Result<u8, Error> {
|
||||
report_density(&mut 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)
|
||||
}
|
||||
|
||||
/// Erase medium.
|
||||
///
|
||||
/// EOD is written at the current position, which marks it as end
|
||||
/// of data. After the command is successfully completed, the
|
||||
/// drive is positioned immediately before End Of Data (not End Of
|
||||
/// Tape).
|
||||
pub fn erase_media(&mut self, fast: bool) -> 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.push(0x19);
|
||||
if fast {
|
||||
cmd.push(0); // LONG=0
|
||||
} else {
|
||||
cmd.push(1); // LONG=1
|
||||
}
|
||||
cmd.extend(&[0, 0, 0, 0]);
|
||||
|
||||
sg_raw.do_command(&cmd)
|
||||
.map_err(|err| format_err!("erase failed - {}", err))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Format media, single partition
|
||||
pub fn format_media(&mut self, fast: bool) -> Result<(), Error> {
|
||||
|
||||
// try to get info about loaded media first
|
||||
let (has_format, is_worm) = match self.read_medium_configuration_page() {
|
||||
Ok((_head, block_descriptor, page)) => {
|
||||
// FORMAT requires LTO5 or newer
|
||||
let has_format = block_descriptor.density_code >= 0x58;
|
||||
let is_worm = page.is_worm();
|
||||
(has_format, is_worm)
|
||||
}
|
||||
Err(_) => {
|
||||
// LTO3 and older do not supprt medium configuration mode page
|
||||
(false, false)
|
||||
}
|
||||
};
|
||||
|
||||
if is_worm {
|
||||
// We cannot FORMAT WORM media! Instead we check if its empty.
|
||||
|
||||
self.move_to_eom(false)?;
|
||||
let pos = self.position()?;
|
||||
if pos.logical_object_number != 0 {
|
||||
bail!("format failed - detected WORM media with data.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
} else {
|
||||
self.rewind()?;
|
||||
|
||||
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
|
||||
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
|
||||
let mut cmd = Vec::new();
|
||||
|
||||
if has_format {
|
||||
cmd.extend(&[0x04, 0, 0, 0, 0, 0]); // FORMAT
|
||||
sg_raw.do_command(&cmd)?;
|
||||
if !fast {
|
||||
self.erase_media(false)?; // overwrite everything
|
||||
}
|
||||
} else {
|
||||
// try rewind/erase instead
|
||||
self.erase_media(fast)?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Lock/Unlock drive door
|
||||
pub fn set_medium_removal(&mut self, allow: bool) -> Result<(), ScsiError> {
|
||||
|
||||
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(&[0x1E, 0, 0, 0]);
|
||||
if allow {
|
||||
cmd.push(0);
|
||||
} else {
|
||||
cmd.push(1);
|
||||
}
|
||||
cmd.push(0); // control
|
||||
|
||||
sg_raw.do_command(&cmd)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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 locate_file(&mut self, position: u64) -> Result<(), Error> {
|
||||
if position == 0 {
|
||||
return self.rewind();
|
||||
}
|
||||
|
||||
const SPACE_ONE_FILEMARK: &[u8] = &[0x11, 0x01, 0, 0, 1, 0];
|
||||
|
||||
// Special case for position 1, because LOCATE 0 does not work
|
||||
if position == 1 {
|
||||
self.rewind()?;
|
||||
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
|
||||
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
|
||||
sg_raw.do_command(SPACE_ONE_FILEMARK)
|
||||
.map_err(|err| format_err!("locate file {} (space) failed - {}", position, err))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
|
||||
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
|
||||
|
||||
// Note: LOCATE(16) works for LTO4 or newer
|
||||
//
|
||||
// It seems the LOCATE command behaves slightly different across vendors
|
||||
// e.g. for IBM drives, LOCATE 1 moves to File #2, but
|
||||
// for HP drives, LOCATE 1 move to File #1
|
||||
|
||||
let fixed_position = if let Some(locate_offset) = self.locate_offset {
|
||||
if locate_offset < 0 {
|
||||
position.saturating_sub((-locate_offset) as u64)
|
||||
} else {
|
||||
position.saturating_add(locate_offset as u64)
|
||||
}
|
||||
} else {
|
||||
position
|
||||
};
|
||||
// always sub(1), so that it works for IBM drives without locate_offset
|
||||
let fixed_position = fixed_position.saturating_sub(1);
|
||||
|
||||
let mut cmd = Vec::new();
|
||||
cmd.extend(&[0x92, 0b000_01_000, 0, 0]); // LOCATE(16) filemarks
|
||||
cmd.extend(&fixed_position.to_be_bytes());
|
||||
cmd.extend(&[0, 0, 0, 0]);
|
||||
|
||||
sg_raw.do_command(&cmd)
|
||||
.map_err(|err| format_err!("locate file {} failed - {}", position, err))?;
|
||||
|
||||
// LOCATE always position at the BOT side of the filemark, so
|
||||
// we need to move to other side of filemark
|
||||
sg_raw.do_command(SPACE_ONE_FILEMARK)
|
||||
.map_err(|err| format_err!("locate file {} (space) failed - {}", position, err))?;
|
||||
|
||||
if self.locate_offset.is_none() {
|
||||
// check if we landed at correct position
|
||||
let current_file = self.current_file_number()?;
|
||||
if current_file != position {
|
||||
let offset: i64 =
|
||||
i64::try_from((position as i128) - (current_file as i128)).map_err(|err| {
|
||||
format_err!(
|
||||
"locate_file: offset between {} and {} invalid: {}",
|
||||
position,
|
||||
current_file,
|
||||
err
|
||||
)
|
||||
})?;
|
||||
self.locate_offset = Some(offset);
|
||||
self.locate_file(position)?;
|
||||
let current_file = self.current_file_number()?;
|
||||
if current_file != position {
|
||||
bail!("locate_file: compensating offset did not work, aborting...");
|
||||
}
|
||||
} else {
|
||||
self.locate_offset = Some(0);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
// READ POSITION LONG FORM works on LTO4 or newer (with recent
|
||||
// firmware), although it is missing in the IBM LTO4 SSCI
|
||||
// reference manual.
|
||||
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");
|
||||
}
|
||||
|
||||
Ok(page)
|
||||
}
|
||||
|
||||
pub fn current_file_number(&mut self) -> Result<u64, Error> {
|
||||
let position = self.position()?;
|
||||
Ok(position.logical_file_id)
|
||||
}
|
||||
|
||||
/// Check if we are positioned after a filemark (or BOT)
|
||||
pub fn check_filemark(&mut self) -> Result<bool, Error> {
|
||||
|
||||
let pos = self.position()?;
|
||||
if pos.logical_object_number == 0 {
|
||||
// at BOT, Ok (no filemark required)
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Note: SPACE blocks returns Err at filemark
|
||||
match self.space(-1, true) {
|
||||
Ok(_) => {
|
||||
self.space(1, true) // move back to end
|
||||
.map_err(|err| format_err!("check_filemark failed (space forward) - {}", err))?;
|
||||
Ok(false)
|
||||
}
|
||||
Err(ScsiError::Sense(SenseInfo { sense_key: 0, asc: 0, ascq: 1 })) => {
|
||||
// Filemark detected - good
|
||||
self.space(1, false) // move to EOT side of filemark
|
||||
.map_err(|err| format_err!("check_filemark failed (move to EOT side of filemark) - {}", err))?;
|
||||
Ok(true)
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("check_filemark failed - {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_to_eom(&mut self, write_missing_eof: bool) -> 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))?;
|
||||
|
||||
if write_missing_eof {
|
||||
if !self.check_filemark()? {
|
||||
self.write_filemarks(1, false)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn space(&mut self, count: isize, blocks: bool) -> Result<(), ScsiError> {
|
||||
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.push(0x11); // SPACE(6)
|
||||
if blocks {
|
||||
cmd.push(0); // blocks
|
||||
} else {
|
||||
cmd.push(1); // 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.push(0x91); // SPACE(16)
|
||||
if blocks {
|
||||
cmd.push(0); // blocks
|
||||
} else {
|
||||
cmd.push(1); // filemarks
|
||||
}
|
||||
cmd.extend(&[0, 0]); // reserved
|
||||
let count: i64 = count as i64;
|
||||
cmd.extend(&count.to_be_bytes());
|
||||
cmd.extend(&[0, 0, 0, 0]); // reserved
|
||||
}
|
||||
|
||||
sg_raw.do_command(&cmd)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn space_filemarks(&mut self, count: isize) -> Result<(), Error> {
|
||||
self.space(count, false)
|
||||
.map_err(|err| format_err!("space filemarks failed - {}", err))
|
||||
}
|
||||
|
||||
pub fn space_blocks(&mut self, count: isize) -> Result<(), Error> {
|
||||
self.space(count, true)
|
||||
.map_err(|err| format_err!("space blocks failed - {}", err))
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
match sg_raw.do_command(&cmd) {
|
||||
Ok(_) => { /* OK */ }
|
||||
Err(ScsiError::Sense(SenseInfo { sense_key: 0, asc: 0, ascq: 2 })) => {
|
||||
/* LEOM - ignore */
|
||||
}
|
||||
Err(err) => {
|
||||
proxmox::io_bail!("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<(), 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
|
||||
|
||||
match sg_raw.do_command(&cmd) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
bail!("test_unit_ready failed - {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(()) => 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> {
|
||||
|
||||
self.encryption_key_loaded = key.is_some();
|
||||
|
||||
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 > 0x800000 {
|
||||
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);
|
||||
|
||||
match sg_raw.do_out_command(&cmd, data) {
|
||||
Ok(()) => { return Ok(false) }
|
||||
Err(ScsiError::Sense(SenseInfo { sense_key: 0, asc: 0, ascq: 2 })) => {
|
||||
return Ok(true); // LEOM
|
||||
}
|
||||
Err(err) => {
|
||||
proxmox::io_bail!("write failed - {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_block(&mut self, buffer: &mut [u8]) -> Result<usize, BlockReadError> {
|
||||
let transfer_len = buffer.len();
|
||||
|
||||
if transfer_len > 0xFFFFFF {
|
||||
return Err(BlockReadError::Error(
|
||||
proxmox::io_format_err!("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 Err(BlockReadError::EndOfFile);
|
||||
}
|
||||
Err(ScsiError::Sense(SenseInfo { sense_key: 8, asc: 0, ascq: 5 })) => {
|
||||
return Err(BlockReadError::EndOfStream);
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(BlockReadError::Error(
|
||||
proxmox::io_format_err!("read failed - {}", err)
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if data.len() != transfer_len {
|
||||
return Err(BlockReadError::Error(
|
||||
proxmox::io_format_err!("read failed - unexpected block len ({} != {})", data.len(), buffer.len())
|
||||
));
|
||||
}
|
||||
|
||||
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<BlockedReader<SgTapeReader>, BlockReadError> {
|
||||
let reader = SgTapeReader::new(self);
|
||||
BlockedReader::open(reader)
|
||||
}
|
||||
|
||||
/// Set all options we need/want
|
||||
pub fn set_default_options(&mut self) -> Result<(), Error> {
|
||||
|
||||
let compression = Some(true);
|
||||
let block_length = Some(0); // variable length mode
|
||||
let buffer_mode = Some(true); // Always use drive buffer
|
||||
|
||||
self.set_drive_options(compression, block_length, buffer_mode)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set important drive options
|
||||
pub fn set_drive_options(
|
||||
&mut self,
|
||||
compression: Option<bool>,
|
||||
block_length: Option<u32>,
|
||||
buffer_mode: Option<bool>,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
// Note: Read/Modify/Write
|
||||
|
||||
let (mut head, mut block_descriptor, mut page) = self.read_compression_page()?;
|
||||
|
||||
let mut sg_raw = SgRaw::new(&mut self.file, 0)?;
|
||||
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
|
||||
|
||||
head.mode_data_len = 0; // need to b e zero
|
||||
|
||||
if let Some(compression) = compression {
|
||||
page.set_compression(compression);
|
||||
}
|
||||
|
||||
if let Some(block_length) = block_length {
|
||||
block_descriptor.set_block_length(block_length)?;
|
||||
}
|
||||
|
||||
if let Some(buffer_mode) = buffer_mode {
|
||||
head.set_buffer_mode(buffer_mode);
|
||||
}
|
||||
|
||||
let mut data = Vec::new();
|
||||
unsafe {
|
||||
data.write_be_value(head)?;
|
||||
data.write_be_value(block_descriptor)?;
|
||||
data.write_be_value(page)?;
|
||||
}
|
||||
|
||||
let mut cmd = Vec::new();
|
||||
cmd.push(0x55); // MODE SELECT(10)
|
||||
cmd.push(0b0001_0000); // PF=1
|
||||
cmd.extend(&[0,0,0,0,0]); //reserved
|
||||
|
||||
let param_list_len: u16 = data.len() as u16;
|
||||
cmd.extend(¶m_list_len.to_be_bytes());
|
||||
cmd.push(0); // control
|
||||
|
||||
let mut buffer = alloc_page_aligned_buffer(4096)?;
|
||||
|
||||
buffer[..data.len()].copy_from_slice(&data[..]);
|
||||
|
||||
sg_raw.do_out_command(&cmd, &buffer[..data.len()])
|
||||
.map_err(|err| format_err!("set drive options failed - {}", err))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_medium_configuration_page(
|
||||
&mut self,
|
||||
) -> Result<(ModeParameterHeader, ModeBlockDescriptor, MediumConfigurationModePage), Error> {
|
||||
|
||||
let (head, block_descriptor, page): (_,_, MediumConfigurationModePage)
|
||||
= scsi_mode_sense(&mut self.file, false, 0x1d, 0)?;
|
||||
|
||||
proxmox::try_block!({
|
||||
if (page.page_code & 0b0011_1111) != 0x1d {
|
||||
bail!("wrong page code {}", page.page_code);
|
||||
}
|
||||
if page.page_length != 0x1e {
|
||||
bail!("wrong page length {}", page.page_length);
|
||||
}
|
||||
|
||||
let block_descriptor = match block_descriptor {
|
||||
Some(block_descriptor) => block_descriptor,
|
||||
None => bail!("missing block descriptor"),
|
||||
};
|
||||
|
||||
Ok((head, block_descriptor, page))
|
||||
}).map_err(|err| format_err!("read_medium_configuration failed - {}", err))
|
||||
}
|
||||
|
||||
fn read_compression_page(
|
||||
&mut self,
|
||||
) -> Result<(ModeParameterHeader, ModeBlockDescriptor, DataCompressionModePage), Error> {
|
||||
|
||||
let (head, block_descriptor, page): (_,_, DataCompressionModePage)
|
||||
= scsi_mode_sense(&mut self.file, false, 0x0f, 0)?;
|
||||
|
||||
proxmox::try_block!({
|
||||
if (page.page_code & 0b0011_1111) != 0x0f {
|
||||
bail!("wrong page code {}", page.page_code);
|
||||
}
|
||||
if page.page_length != 0x0e {
|
||||
bail!("wrong page length {}", page.page_length);
|
||||
}
|
||||
|
||||
let block_descriptor = match block_descriptor {
|
||||
Some(block_descriptor) => block_descriptor,
|
||||
None => bail!("missing block descriptor"),
|
||||
};
|
||||
|
||||
Ok((head, block_descriptor, page))
|
||||
}).map_err(|err| format_err!("read_compression_page failed: {}", err))
|
||||
}
|
||||
|
||||
/// Read drive options/status
|
||||
///
|
||||
/// We read the drive compression page, including the
|
||||
/// block_descriptor. This is all information we need for now.
|
||||
pub fn read_drive_status(&mut self) -> Result<LtoTapeStatus, Error> {
|
||||
|
||||
// We do a Request Sense, but ignore the result.
|
||||
// This clears deferred error or media changed events.
|
||||
let _ = scsi_request_sense(&mut self.file);
|
||||
|
||||
let (head, block_descriptor, page) = self.read_compression_page()?;
|
||||
|
||||
Ok(LtoTapeStatus {
|
||||
block_length: block_descriptor.block_length(),
|
||||
write_protect: head.write_protect(),
|
||||
buffer_mode: head.buffer_mode(),
|
||||
compression: page.compression_enabled(),
|
||||
density_code: block_descriptor.density_code,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get Tape and Media status
|
||||
pub fn get_drive_and_media_status(&mut self) -> Result<LtoDriveAndMediaStatus, Error> {
|
||||
|
||||
let drive_status = self.read_drive_status()?;
|
||||
|
||||
let alert_flags = self.tape_alert_flags()
|
||||
.map(|flags| format!("{:?}", flags))
|
||||
.ok();
|
||||
|
||||
let mut status = LtoDriveAndMediaStatus {
|
||||
vendor: self.info().vendor.clone(),
|
||||
product: self.info().product.clone(),
|
||||
revision: self.info().revision.clone(),
|
||||
blocksize: drive_status.block_length,
|
||||
compression: drive_status.compression,
|
||||
buffer_mode: drive_status.buffer_mode,
|
||||
density: drive_status.density_code.try_into()?,
|
||||
alert_flags,
|
||||
write_protect: None,
|
||||
file_number: None,
|
||||
block_number: None,
|
||||
manufactured: None,
|
||||
bytes_read: None,
|
||||
bytes_written: None,
|
||||
medium_passes: None,
|
||||
medium_wearout: None,
|
||||
volume_mounts: None,
|
||||
};
|
||||
|
||||
if self.test_unit_ready().is_ok() {
|
||||
|
||||
if drive_status.write_protect {
|
||||
status.write_protect = Some(drive_status.write_protect);
|
||||
}
|
||||
|
||||
let position = self.position()?;
|
||||
|
||||
status.file_number = Some(position.logical_file_id);
|
||||
status.block_number = Some(position.logical_object_number);
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Drop for SgTape {
|
||||
fn drop(&mut self) {
|
||||
// For security reasons, clear the encryption key
|
||||
if self.encryption_key_loaded {
|
||||
let _ = self.set_encryption(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct SgTapeReader<'a> {
|
||||
sg_tape: &'a mut SgTape,
|
||||
end_of_file: bool,
|
||||
}
|
||||
|
||||
impl <'a> SgTapeReader<'a> {
|
||||
|
||||
pub fn new(sg_tape: &'a mut SgTape) -> Self {
|
||||
Self { sg_tape, end_of_file: false, }
|
||||
}
|
||||
}
|
||||
|
||||
impl <'a> BlockRead for SgTapeReader<'a> {
|
||||
|
||||
fn read_block(&mut self, buffer: &mut [u8]) -> Result<usize, BlockReadError> {
|
||||
if self.end_of_file {
|
||||
return Err(BlockReadError::Error(proxmox::io_format_err!("detected read after EOF!")));
|
||||
}
|
||||
match self.sg_tape.read_block(buffer) {
|
||||
Ok(usize) => Ok(usize),
|
||||
Err(BlockReadError::EndOfFile) => {
|
||||
self.end_of_file = true;
|
||||
Err(BlockReadError::EndOfFile)
|
||||
},
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
293
pbs-tape/src/sg_tape/encryption.rs
Normal file
293
pbs-tape/src/sg_tape/encryption.rs
Normal file
@ -0,0 +1,293 @@
|
||||
use std::os::unix::prelude::AsRawFd;
|
||||
use std::io::Write;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use endian_trait::Endian;
|
||||
|
||||
use proxmox::tools::io::{ReadExt, WriteExt};
|
||||
|
||||
use crate::sgutils2::{SgRaw, alloc_page_aligned_buffer};
|
||||
|
||||
/// Test if drive supports hardware encryption
|
||||
///
|
||||
/// We search for AES_CGM algorithm with 256bits key.
|
||||
pub fn has_encryption<F: AsRawFd>(
|
||||
file: &mut F,
|
||||
) -> bool {
|
||||
|
||||
let data = match sg_spin_data_encryption_caps(file) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return false,
|
||||
};
|
||||
decode_spin_data_encryption_caps(&data).is_ok()
|
||||
}
|
||||
|
||||
/// Set or clear encryption key
|
||||
///
|
||||
/// We always use mixed mode,
|
||||
pub fn set_encryption<F: AsRawFd>(
|
||||
file: &mut F,
|
||||
key: Option<[u8; 32]>,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let data = match sg_spin_data_encryption_caps(file) {
|
||||
Ok(data) => data,
|
||||
Err(_) if key.is_none() => {
|
||||
// Assume device does not support HW encryption
|
||||
// We can simply ignore the clear key request
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
let algorithm_index = decode_spin_data_encryption_caps(&data)?;
|
||||
|
||||
sg_spout_set_encryption(file, algorithm_index, key)?;
|
||||
|
||||
let data = sg_spin_data_encryption_status(file)?;
|
||||
let status = decode_spin_data_encryption_status(&data)?;
|
||||
|
||||
match status.mode {
|
||||
DataEncryptionMode::Off => {
|
||||
if key.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
DataEncryptionMode::Mixed => {
|
||||
if key.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
bail!("got unexpected encryption mode {:?}", status.mode);
|
||||
}
|
||||
|
||||
#[derive(Endian)]
|
||||
#[repr(C, packed)]
|
||||
struct SspSetDataEncryptionPage {
|
||||
page_code: u16,
|
||||
page_len: u16,
|
||||
scope_byte: u8,
|
||||
control_byte_5: u8,
|
||||
encryption_mode: u8,
|
||||
decryption_mode: u8,
|
||||
algorythm_index: u8,
|
||||
key_format: u8,
|
||||
reserved: [u8; 8],
|
||||
key_len: u16,
|
||||
/* key follows */
|
||||
}
|
||||
|
||||
fn sg_spout_set_encryption<F: AsRawFd>(
|
||||
file: &mut F,
|
||||
algorythm_index: u8,
|
||||
key: Option<[u8; 32]>,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let mut sg_raw = SgRaw::new(file, 0)?;
|
||||
|
||||
let mut outbuf_len = std::mem::size_of::<SspSetDataEncryptionPage>();
|
||||
if let Some(ref key) = key {
|
||||
outbuf_len += key.len();
|
||||
}
|
||||
|
||||
let mut outbuf = alloc_page_aligned_buffer(outbuf_len)?;
|
||||
let chok: u8 = 0;
|
||||
|
||||
let page = SspSetDataEncryptionPage {
|
||||
page_code: 0x10,
|
||||
page_len: (outbuf_len - 4) as u16,
|
||||
scope_byte: (0b10 << 5), // all IT nexus
|
||||
control_byte_5: (chok << 2),
|
||||
encryption_mode: if key.is_some() { 2 } else { 0 },
|
||||
decryption_mode: if key.is_some() { 3 } else { 0 }, // mixed mode
|
||||
algorythm_index,
|
||||
key_format: 0,
|
||||
reserved: [0u8; 8],
|
||||
key_len: if let Some(ref key) = key { key.len() as u16 } else { 0 },
|
||||
};
|
||||
|
||||
let mut writer = &mut outbuf[..];
|
||||
unsafe { writer.write_be_value(page)? };
|
||||
|
||||
if let Some(ref key) = key {
|
||||
writer.write_all(key)?;
|
||||
}
|
||||
|
||||
let mut cmd = Vec::new();
|
||||
cmd.push(0xB5); // SECURITY PROTOCOL IN (SPOUT)
|
||||
cmd.push(0x20); // Tape Data Encryption Page
|
||||
cmd.push(0);cmd.push(0x10); // Set Data Encryption page
|
||||
cmd.push(0);
|
||||
cmd.push(0);
|
||||
cmd.extend(&(outbuf_len as u32).to_be_bytes()); // data out len
|
||||
cmd.push(0);
|
||||
cmd.push(0);
|
||||
|
||||
sg_raw.do_out_command(&cmd, &outbuf)
|
||||
.map_err(|err| format_err!("set data encryption SPOUT(20h[0010h]) failed - {}", err))
|
||||
}
|
||||
|
||||
// Warning: this blocks and fails if there is no media loaded
|
||||
fn sg_spin_data_encryption_status<F: AsRawFd>(file: &mut F) -> Result<Vec<u8>, Error> {
|
||||
|
||||
let allocation_len: u32 = 8192+4;
|
||||
|
||||
let mut sg_raw = SgRaw::new(file, allocation_len as usize)?;
|
||||
|
||||
let mut cmd = Vec::new();
|
||||
cmd.push(0xA2); // SECURITY PROTOCOL IN (SPIN)
|
||||
cmd.push(0x20); // Tape Data Encryption Page
|
||||
cmd.push(0);cmd.push(0x20); // Data Encryption Status page
|
||||
cmd.push(0);
|
||||
cmd.push(0);
|
||||
cmd.extend(&allocation_len.to_be_bytes());
|
||||
cmd.push(0);
|
||||
cmd.push(0);
|
||||
|
||||
sg_raw.do_command(&cmd)
|
||||
.map_err(|err| format_err!("read data encryption status SPIN(20h[0020h]) failed - {}", err))
|
||||
.map(|v| v.to_vec())
|
||||
}
|
||||
|
||||
// Warning: this blocks and fails if there is no media loaded
|
||||
fn sg_spin_data_encryption_caps<F: AsRawFd>(file: &mut F) -> Result<Vec<u8>, Error> {
|
||||
|
||||
let allocation_len: u32 = 8192+4;
|
||||
|
||||
let mut sg_raw = SgRaw::new(file, allocation_len as usize)?;
|
||||
|
||||
let mut cmd = Vec::new();
|
||||
cmd.push(0xA2); // SECURITY PROTOCOL IN (SPIN)
|
||||
cmd.push(0x20); // Tape Data Encryption Page
|
||||
cmd.push(0);cmd.push(0x10); // Data Encryption Capabilities page
|
||||
cmd.push(0);
|
||||
cmd.push(0);
|
||||
cmd.extend(&allocation_len.to_be_bytes());
|
||||
cmd.push(0);
|
||||
cmd.push(0);
|
||||
|
||||
sg_raw.do_command(&cmd)
|
||||
.map_err(|err| format_err!("read data encryption caps SPIN(20h[0010h]) failed - {}", err))
|
||||
.map(|v| v.to_vec())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DataEncryptionMode {
|
||||
On,
|
||||
Mixed,
|
||||
RawRead,
|
||||
Off,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DataEncryptionStatus {
|
||||
mode: DataEncryptionMode,
|
||||
}
|
||||
|
||||
#[derive(Endian)]
|
||||
#[repr(C, packed)]
|
||||
struct SspDataEncryptionCapabilityPage {
|
||||
page_code: u16,
|
||||
page_len: u16,
|
||||
reserved: [u8; 16],
|
||||
}
|
||||
|
||||
#[derive(Endian)]
|
||||
#[repr(C, packed)]
|
||||
struct SspDataEncryptionAlgorithmDescriptor {
|
||||
algorythm_index: u8,
|
||||
reserved1: u8,
|
||||
descriptor_len: u16,
|
||||
control_byte_4: u8,
|
||||
control_byte_5: u8,
|
||||
max_ucad_bytes: u16,
|
||||
max_acad_bytes: u16,
|
||||
key_size: u16,
|
||||
control_byte_12: u8,
|
||||
reserved2: u8,
|
||||
msdk_count: u16,
|
||||
reserved3: [u8; 4],
|
||||
algorithm_code: u32,
|
||||
}
|
||||
|
||||
// Returns the algorythm_index for AES-CGM
|
||||
fn decode_spin_data_encryption_caps(data: &[u8]) -> Result<u8, Error> {
|
||||
|
||||
proxmox::try_block!({
|
||||
let mut reader = &data[..];
|
||||
let _page: SspDataEncryptionCapabilityPage = unsafe { reader.read_be_value()? };
|
||||
|
||||
let mut aes_cgm_index = None;
|
||||
|
||||
loop {
|
||||
if reader.is_empty() { break; };
|
||||
let desc: SspDataEncryptionAlgorithmDescriptor =
|
||||
unsafe { reader.read_be_value()? };
|
||||
if desc.descriptor_len != 0x14 {
|
||||
bail!("got wrong key descriptor len");
|
||||
}
|
||||
if (desc.control_byte_4 & 0b00000011) != 2 {
|
||||
continue; // can't encrypt in hardware
|
||||
}
|
||||
if ((desc.control_byte_4 & 0b00001100) >> 2) != 2 {
|
||||
continue; // can't decrypt in hardware
|
||||
}
|
||||
if desc.algorithm_code == 0x00010014 && desc.key_size == 32 {
|
||||
aes_cgm_index = Some(desc.algorythm_index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
match aes_cgm_index {
|
||||
Some(index) => Ok(index),
|
||||
None => bail!("drive does not support AES-CGM encryption"),
|
||||
}
|
||||
}).map_err(|err: Error| format_err!("decode data encryption caps page failed - {}", err))
|
||||
|
||||
}
|
||||
|
||||
#[derive(Endian)]
|
||||
#[repr(C, packed)]
|
||||
struct SspDataEncryptionStatusPage {
|
||||
page_code: u16,
|
||||
page_len: u16,
|
||||
scope_byte: u8,
|
||||
encryption_mode: u8,
|
||||
decryption_mode: u8,
|
||||
algorythm_index: u8,
|
||||
key_instance_counter: u32,
|
||||
control_byte: u8,
|
||||
key_format: u8,
|
||||
key_len: u16,
|
||||
reserved: [u8; 8],
|
||||
}
|
||||
|
||||
fn decode_spin_data_encryption_status(data: &[u8]) -> Result<DataEncryptionStatus, Error> {
|
||||
|
||||
proxmox::try_block!({
|
||||
let mut reader = &data[..];
|
||||
let page: SspDataEncryptionStatusPage = unsafe { reader.read_be_value()? };
|
||||
|
||||
if page.page_code != 0x20 {
|
||||
bail!("invalid response");
|
||||
}
|
||||
|
||||
let mode = match (page.encryption_mode, page.decryption_mode) {
|
||||
(0, 0) => DataEncryptionMode::Off,
|
||||
(2, 1) => DataEncryptionMode::RawRead,
|
||||
(2, 2) => DataEncryptionMode::On,
|
||||
(2, 3) => DataEncryptionMode::Mixed,
|
||||
_ => bail!("unknown encryption mode"),
|
||||
};
|
||||
|
||||
let status = DataEncryptionStatus {
|
||||
mode,
|
||||
};
|
||||
|
||||
Ok(status)
|
||||
|
||||
}).map_err(|err| format_err!("decode data encryption status page failed - {}", err))
|
||||
}
|
236
pbs-tape/src/sg_tape/mam.rs
Normal file
236
pbs-tape/src/sg_tape/mam.rs
Normal file
@ -0,0 +1,236 @@
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use endian_trait::Endian;
|
||||
|
||||
use proxmox::tools::io::ReadExt;
|
||||
|
||||
use pbs_api_types::MamAttribute;
|
||||
|
||||
use crate::sgutils2::SgRaw;
|
||||
|
||||
use super::TapeAlertFlags;
|
||||
|
||||
// Read Medium auxiliary memory attributes (MAM)
|
||||
// see IBM SCSI reference: https://www-01.ibm.com/support/docview.wss?uid=ssg1S7003556&aid=1
|
||||
|
||||
#[derive(Endian)]
|
||||
#[repr(C,packed)]
|
||||
struct MamAttributeHeader {
|
||||
id: u16,
|
||||
flags: u8,
|
||||
len: u16,
|
||||
}
|
||||
|
||||
enum MamFormat {
|
||||
BINARY,
|
||||
ASCII,
|
||||
DEC,
|
||||
}
|
||||
|
||||
static MAM_ATTRIBUTES: &[ (u16, u16, MamFormat, &str) ] = &[
|
||||
(0x00_00, 8, MamFormat::DEC, "Remaining Capacity In Partition"),
|
||||
(0x00_01, 8, MamFormat::DEC, "Maximum Capacity In Partition"),
|
||||
(0x00_02, 8, MamFormat::DEC, "Tapealert Flags"),
|
||||
(0x00_03, 8, MamFormat::DEC, "Load Count"),
|
||||
(0x00_04, 8, MamFormat::DEC, "MAM Space Remaining"),
|
||||
(0x00_05, 8, MamFormat::ASCII, "Assigning Organization"),
|
||||
(0x00_06, 1, MamFormat::BINARY, "Formatted Density Code"),
|
||||
(0x00_07, 2, MamFormat::DEC, "Initialization Count"),
|
||||
(0x00_09, 4, MamFormat::BINARY, "Volume Change Reference"),
|
||||
|
||||
(0x02_0A, 40, MamFormat::ASCII, "Device Vendor/Serial Number at Last Load"),
|
||||
(0x02_0B, 40, MamFormat::ASCII, "Device Vendor/Serial Number at Load-1"),
|
||||
(0x02_0C, 40, MamFormat::ASCII, "Device Vendor/Serial Number at Load-2"),
|
||||
(0x02_0D, 40, MamFormat::ASCII, "Device Vendor/Serial Number at Load-3"),
|
||||
|
||||
(0x02_20, 8, MamFormat::DEC, "Total MBytes Written in Medium Life"),
|
||||
(0x02_21, 8, MamFormat::DEC, "Total MBytes Read In Medium Life"),
|
||||
(0x02_22, 8, MamFormat::DEC, "Total MBytes Written in Current Load"),
|
||||
(0x02_23, 8, MamFormat::DEC, "Total MBytes Read in Current/Last Load"),
|
||||
(0x02_24, 8, MamFormat::BINARY, "Logical Position of First Encrypted Block"),
|
||||
(0x02_25, 8, MamFormat::BINARY, "Logical Position of First Unencrypted Block After the First Encrypted Block"),
|
||||
|
||||
(0x04_00, 8, MamFormat::ASCII, "Medium Manufacturer"),
|
||||
(0x04_01, 32, MamFormat::ASCII, "Medium Serial Number"),
|
||||
(0x04_02, 4, MamFormat::DEC, "Medium Length"),
|
||||
(0x04_03, 4, MamFormat::DEC, "Medium Width"),
|
||||
(0x04_04, 8, MamFormat::ASCII, "Assigning Organization"),
|
||||
(0x04_05, 1, MamFormat::BINARY, "Medium Density Code"),
|
||||
(0x04_06, 8, MamFormat::ASCII, "Medium Manufacture Date"),
|
||||
(0x04_07, 8, MamFormat::DEC, "MAM Capacity"),
|
||||
(0x04_08, 1, MamFormat::BINARY, "Medium Type"),
|
||||
(0x04_09, 2, MamFormat::BINARY, "Medium Type Information"),
|
||||
(0x04_0B, 10, MamFormat::BINARY, "Supported Density Codes"),
|
||||
|
||||
(0x08_00, 8, MamFormat::ASCII, "Application Vendor"),
|
||||
(0x08_01, 32, MamFormat::ASCII, "Application Name"),
|
||||
(0x08_02, 8, MamFormat::ASCII, "Application Version"),
|
||||
(0x08_03, 160, MamFormat::ASCII, "User Medium Text Label"),
|
||||
(0x08_04, 12, MamFormat::ASCII, "Date And Time Last Written"),
|
||||
(0x08_05, 1, MamFormat::BINARY, "Text Localization Identifier"),
|
||||
(0x08_06, 32, MamFormat::ASCII, "Barcode"),
|
||||
(0x08_07, 80, MamFormat::ASCII, "Owning Host Textual Name"),
|
||||
(0x08_08, 160, MamFormat::ASCII, "Media Pool"),
|
||||
(0x08_0B, 16, MamFormat::ASCII, "Application Format Version"),
|
||||
(0x08_0C, 50, MamFormat::ASCII, "Volume Coherency Information"),
|
||||
(0x08_20, 36, MamFormat::ASCII, "Medium Globally Unique Identifier"),
|
||||
(0x08_21, 36, MamFormat::ASCII, "Media Pool Globally Unique Identifier"),
|
||||
|
||||
(0x10_00, 28, MamFormat::BINARY, "Unique Cartridge Identify (UCI)"),
|
||||
(0x10_01, 24, MamFormat::BINARY, "Alternate Unique Cartridge Identify (Alt-UCI)"),
|
||||
|
||||
];
|
||||
|
||||
lazy_static::lazy_static!{
|
||||
|
||||
static ref MAM_ATTRIBUTE_NAMES: HashMap<u16, &'static (u16, u16, MamFormat, &'static str)> = {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
for entry in MAM_ATTRIBUTES {
|
||||
map.insert(entry.0, entry);
|
||||
}
|
||||
|
||||
map
|
||||
};
|
||||
}
|
||||
|
||||
fn read_tape_mam<F: AsRawFd>(file: &mut F) -> Result<Vec<u8>, Error> {
|
||||
|
||||
let alloc_len: u32 = 32*1024;
|
||||
let mut sg_raw = SgRaw::new(file, alloc_len as usize)?;
|
||||
|
||||
let mut cmd = Vec::new();
|
||||
cmd.extend(&[0x8c, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8]);
|
||||
cmd.extend(&[0u8, 0u8]); // first attribute
|
||||
cmd.extend(&alloc_len.to_be_bytes()); // alloc len
|
||||
cmd.extend(&[0u8, 0u8]);
|
||||
|
||||
sg_raw.do_command(&cmd)
|
||||
.map_err(|err| format_err!("read cartidge memory failed - {}", err))
|
||||
.map(|v| v.to_vec())
|
||||
}
|
||||
|
||||
/// Read Medium auxiliary memory attributes (cartridge memory) using raw SCSI command.
|
||||
pub fn read_mam_attributes<F: AsRawFd>(file: &mut F) -> Result<Vec<MamAttribute>, Error> {
|
||||
|
||||
let data = read_tape_mam(file)?;
|
||||
|
||||
decode_mam_attributes(&data)
|
||||
}
|
||||
|
||||
fn decode_mam_attributes(data: &[u8]) -> Result<Vec<MamAttribute>, Error> {
|
||||
|
||||
let mut reader = &data[..];
|
||||
|
||||
let data_len: u32 = unsafe { reader.read_be_value()? };
|
||||
|
||||
let expected_len = data_len as usize;
|
||||
|
||||
|
||||
if reader.len() < expected_len {
|
||||
bail!("read_mam_attributes: got unexpected data len ({} != {})", reader.len(), expected_len);
|
||||
} else if reader.len() > expected_len {
|
||||
// Note: Quantum hh7 returns the allocation_length instead of real data_len
|
||||
reader = &data[4..expected_len+4];
|
||||
}
|
||||
|
||||
let mut list = Vec::new();
|
||||
|
||||
loop {
|
||||
if reader.is_empty() {
|
||||
break;
|
||||
}
|
||||
let head: MamAttributeHeader = unsafe { reader.read_be_value()? };
|
||||
//println!("GOT ID {:04X} {:08b} {}", head.id, head.flags, head.len);
|
||||
|
||||
let head_id = head.id;
|
||||
|
||||
let data = if head.len > 0 {
|
||||
reader.read_exact_allocated(head.len as usize)?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
if let Some(info) = MAM_ATTRIBUTE_NAMES.get(&head_id) {
|
||||
if info.1 == head.len {
|
||||
let value = match info.2 {
|
||||
MamFormat::ASCII => String::from_utf8_lossy(&data).to_string(),
|
||||
MamFormat::DEC => {
|
||||
if info.1 == 2 {
|
||||
format!("{}", u16::from_be_bytes(data[0..2].try_into()?))
|
||||
} else if info.1 == 4 {
|
||||
format!("{}", u32::from_be_bytes(data[0..4].try_into()?))
|
||||
} else if info.1 == 8 {
|
||||
if head_id == 2 { // Tape Alert Flags
|
||||
let value = u64::from_be_bytes(data[0..8].try_into()?);
|
||||
let flags = TapeAlertFlags::from_bits_truncate(value);
|
||||
format!("{:?}", flags)
|
||||
} else {
|
||||
format!("{}", u64::from_be_bytes(data[0..8].try_into()?))
|
||||
}
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
},
|
||||
MamFormat::BINARY => proxmox::tools::digest_to_hex(&data),
|
||||
};
|
||||
list.push(MamAttribute {
|
||||
id: head_id,
|
||||
name: info.3.to_string(),
|
||||
value,
|
||||
});
|
||||
} else {
|
||||
eprintln!("read_mam_attributes: got starnge data len for id {:04X}", head_id);
|
||||
}
|
||||
} else {
|
||||
// skip unknown IDs
|
||||
}
|
||||
}
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Media Usage Information from Cartridge Memory
|
||||
pub struct MediaUsageInfo {
|
||||
pub manufactured: i64,
|
||||
pub bytes_read: u64,
|
||||
pub bytes_written: u64,
|
||||
}
|
||||
|
||||
/// Extract Media Usage Information from Cartridge Memory
|
||||
pub fn mam_extract_media_usage(mam: &[MamAttribute]) -> Result<MediaUsageInfo, Error> {
|
||||
|
||||
let manufactured: i64 = match mam.iter().find(|v| v.id == 0x04_06).map(|v| v.value.clone()) {
|
||||
Some(date_str) => {
|
||||
if date_str.len() != 8 {
|
||||
bail!("unable to parse 'Medium Manufacture Date' - wrong length");
|
||||
}
|
||||
let year: i32 = date_str[..4].parse()?;
|
||||
let mon: i32 = date_str[4..6].parse()?;
|
||||
let mday: i32 = date_str[6..8].parse()?;
|
||||
|
||||
use proxmox::tools::time::TmEditor;
|
||||
let mut t = TmEditor::new(true);
|
||||
t.set_year(year)?;
|
||||
t.set_mon(mon)?;
|
||||
t.set_mday(mday)?;
|
||||
|
||||
t.into_epoch()?
|
||||
}
|
||||
None => bail!("unable to read MAM 'Medium Manufacture Date'"),
|
||||
};
|
||||
|
||||
let bytes_written: u64 = match mam.iter().find(|v| v.id == 0x02_20).map(|v| v.value.clone()) {
|
||||
Some(read_str) => read_str.parse::<u64>()? * 1024*1024,
|
||||
None => bail!("unable to read MAM 'Total MBytes Written In Medium Life'"),
|
||||
};
|
||||
|
||||
let bytes_read: u64 = match mam.iter().find(|v| v.id == 0x02_21).map(|v| v.value.clone()) {
|
||||
Some(read_str) => read_str.parse::<u64>()? * 1024*1024,
|
||||
None => bail!("unable to read MAM 'Total MBytes Read In Medium Life'"),
|
||||
};
|
||||
|
||||
Ok(MediaUsageInfo { manufactured, bytes_written, bytes_read })
|
||||
}
|
69
pbs-tape/src/sg_tape/report_density.rs
Normal file
69
pbs-tape/src/sg_tape/report_density.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use std::io::Read;
|
||||
use endian_trait::Endian;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
|
||||
use proxmox::tools::io::ReadExt;
|
||||
|
||||
use crate::sgutils2::SgRaw;
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Endian)]
|
||||
struct DesnityDescriptorBlock {
|
||||
primary_density_code: u8,
|
||||
secondary_density_code: u8,
|
||||
flags2: u8,
|
||||
reserved: [u8; 2],
|
||||
bits_per_mm: [u8; 3],
|
||||
media_width: u16,
|
||||
tracks: u16,
|
||||
capacity: u32,
|
||||
organizazion: [u8; 8],
|
||||
density_name: [u8; 8],
|
||||
description: [u8; 20],
|
||||
}
|
||||
|
||||
// Returns the maximum supported drive density code
|
||||
pub fn report_density<F: AsRawFd>(file: &mut F) -> Result<u8, Error> {
|
||||
let alloc_len: u16 = 8192;
|
||||
let mut sg_raw = SgRaw::new(file, alloc_len as usize)?;
|
||||
|
||||
let mut cmd = Vec::new();
|
||||
cmd.extend(&[0x44, 0, 0, 0, 0, 0, 0]); // REPORT DENSITY SUPPORT (MEDIA=0)
|
||||
cmd.extend(&alloc_len.to_be_bytes()); // alloc len
|
||||
cmd.push(0u8); // control byte
|
||||
|
||||
let data = sg_raw.do_command(&cmd)
|
||||
.map_err(|err| format_err!("report density failed - {}", err))?;
|
||||
|
||||
let mut max_density = 0u8;
|
||||
|
||||
proxmox::try_block!({
|
||||
let mut reader = &data[..];
|
||||
|
||||
let page_len: u16 = unsafe { reader.read_be_value()? };
|
||||
let page_len = page_len as usize;
|
||||
|
||||
if (page_len + 2) > data.len() {
|
||||
bail!("invalid page length {} {}", page_len + 2, data.len());
|
||||
} else {
|
||||
// Note: Quantum hh7 returns the allocation_length instead of real data_len
|
||||
reader = &data[2..page_len+2];
|
||||
}
|
||||
let mut reserved = [0u8; 2];
|
||||
reader.read_exact(&mut reserved)?;
|
||||
|
||||
loop {
|
||||
if reader.is_empty() { break; }
|
||||
let block: DesnityDescriptorBlock = unsafe { reader.read_be_value()? };
|
||||
if block.primary_density_code > max_density {
|
||||
max_density = block.primary_density_code;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
}).map_err(|err| format_err!("decode report density failed - {}", err))?;
|
||||
|
||||
Ok(max_density)
|
||||
}
|
185
pbs-tape/src/sg_tape/tape_alert_flags.rs
Normal file
185
pbs-tape/src/sg_tape/tape_alert_flags.rs
Normal file
@ -0,0 +1,185 @@
|
||||
use std::io::Read;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
|
||||
use proxmox::tools::io::ReadExt;
|
||||
|
||||
use crate::sgutils2::SgRaw;
|
||||
|
||||
bitflags::bitflags!{
|
||||
|
||||
/// Tape Alert Flags
|
||||
///
|
||||
/// See LTO SCSI Reference LOG_SENSE - LP 2Eh: TapeAlerts
|
||||
pub struct TapeAlertFlags: u64 {
|
||||
#[allow(clippy::eq_op)]
|
||||
const READ_WARNING = 1 << (0x0001 -1);
|
||||
const WRITE_WARNING = 1 << (0x0002 -1);
|
||||
const HARD_ERROR = 1 << (0x0003 -1);
|
||||
const MEDIA = 1 << (0x0004 -1);
|
||||
const READ_FAILURE = 1 << (0x0005 -1);
|
||||
const WRITE_FAILURE = 1 << (0x0006 -1);
|
||||
const MEDIA_LIFE = 1 << (0x0007 -1);
|
||||
const NOT_DATA_GRADE = 1 << (0x0008 -1);
|
||||
const WRITE_PROTECT = 1 << (0x0009 -1);
|
||||
const NO_REMOVAL = 1 << (0x000A -1);
|
||||
const CLEANING_MEDIA = 1 << (0x000B -1);
|
||||
const UNSUPPORTED_FORMAT = 1 << (0x000C -1);
|
||||
const RECOVERABLE_MECHANICAL_CARTRIDGE_FAILURE = 1 << (0x000D -1); // LTO5
|
||||
const UNRECOVERABLE_SNAPPED_TAPE = 1 << (0x000E -1);
|
||||
const MEMORY_CHIP_IN_CARTRIDGE_FAILURE = 1 << (0x000F -1);
|
||||
const FORCED_EJECT = 1 << (0x0010 -1);
|
||||
const READ_ONLY_FORMAT = 1 << (0x0011 -1);
|
||||
const TAPE_DIRECTORY_CORRUPTED = 1 << (0x0012 -1);
|
||||
const NEARING_MEDIA_LIFE = 1 << (0x0013 -1);
|
||||
const CLEAN_NOW = 1 << (0x0014 -1);
|
||||
const CLEAN_PERIODIC = 1 << (0x0015 -1);
|
||||
const EXPIRED_CLEANING_MEDIA = 1 << (0x0016 -1);
|
||||
const INVALID_CLEANING_TAPE = 1 << (0x0017 -1);
|
||||
const RETENSION_REQUEST = 1 << (0x0018 -1); // LTO5
|
||||
const HOST_CHANNEL_FAILURE = 1 << (0x0019 -1);
|
||||
const COOLING_FAN_FAILURE = 1 << (0x001A -1);
|
||||
const POWER_SUPPLY_FAILURE = 1 << (0x001B -1);
|
||||
const POWER_CONSUMPTION = 1 << (0x001C -1); // LTO5
|
||||
const DRIVE_MANTAINANCE = 1 << (0x001D -1); // LTO5
|
||||
const HARDWARE_A = 1 << (0x001E -1);
|
||||
const HARDWARE_B = 1 << (0x001F -1);
|
||||
const INTERFACE = 1 << (0x0020 -1);
|
||||
const EJECT_MEDIA = 1 << (0x0021 -1);
|
||||
const DOWNLOAD_FAULT = 1 << (0x0022 -1);
|
||||
const DRIVE_HUMIDITY = 1 << (0x0023 -1); // LTO5
|
||||
const DRIVE_TEMPERATURE = 1 << (0x0024 -1);
|
||||
const DRIVE_VOLTAGE = 1 << (0x0025 -1);
|
||||
const PREDICTIVE_FAILURE = 1 << (0x0026 -1);
|
||||
const DIAGNOSTICS_REQUIRED = 1 << (0x0027 -1);
|
||||
const LOADER_STRAY_TAPE = 1 << (0x0029 -1);
|
||||
const LOADER_HARDWARE = 1 << (0x002A -1);
|
||||
const LOADER_MAGAZINE = 1 << (0x002D -1);
|
||||
const DIMINISHED_NATIVE_CAPACITY = 1 << (0x0031 -1);
|
||||
const LOST_STATISTICS = 1 << (0x0032 -1);
|
||||
const TAPE_DIRECTORY_INVALID_AT_UNLOAD = 1 << (0x0033 -1);
|
||||
const TAPE_SYSTEM_AREA_WRITE_FAILURE = 1 << (0x0034 -1);
|
||||
const TAPE_SYSTEM_AREA_READ_FAILURE = 1 << (0x0035 -1);
|
||||
const NO_START_OF_DATA = 1 << (0x0036 -1);
|
||||
const LOADING_FAILURE = 1 << (0x0037 -1);
|
||||
const UNRECOVERABLE_UNLOAD_FAILURE = 1 << (0x0038 -1);
|
||||
const AUTOMATION_INTERFACE_FAILURE = 1 << (0x0039 -1);
|
||||
const FIRMWARE_FAILURE = 1 << (0x003A -1);
|
||||
const WORM_INTEGRITY_CHECK_FAILED = 1 << (0x003B -1);
|
||||
const WORM_OVERWRITE_ATTEMPTED = 1 << (0x003C -1);
|
||||
const ENCRYPTION_POLICY_VIOLATION = 1 << (0x003D -1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Read Tape Alert Flags using raw SCSI command.
|
||||
pub fn read_tape_alert_flags<F: AsRawFd>(file: &mut F) -> Result<TapeAlertFlags, Error> {
|
||||
|
||||
let data = sg_read_tape_alert_flags(file)?;
|
||||
|
||||
decode_tape_alert_flags(&data)
|
||||
}
|
||||
|
||||
|
||||
fn sg_read_tape_alert_flags<F: AsRawFd>(file: &mut F) -> Result<Vec<u8>, Error> {
|
||||
|
||||
let mut sg_raw = SgRaw::new(file, 512)?;
|
||||
|
||||
// Note: We cannjot use LP 2Eh TapeAlerts, because that clears flags on read.
|
||||
// Instead, we use LP 12h TapeAlert Response. which does not clear the flags.
|
||||
|
||||
let mut cmd = Vec::new();
|
||||
cmd.push(0x4D); // LOG SENSE
|
||||
cmd.push(0);
|
||||
cmd.push((1<<6) | 0x12); // Tape Alert Response log page
|
||||
cmd.push(0);
|
||||
cmd.push(0);
|
||||
cmd.push(0);
|
||||
cmd.push(0);
|
||||
cmd.extend(&[2u8, 0u8]); // alloc len
|
||||
cmd.push(0u8); // control byte
|
||||
|
||||
sg_raw.do_command(&cmd)
|
||||
.map_err(|err| format_err!("read tape alert flags failed - {}", err))
|
||||
.map(|v| v.to_vec())
|
||||
}
|
||||
|
||||
fn decode_tape_alert_flags(data: &[u8]) -> Result<TapeAlertFlags, Error> {
|
||||
|
||||
proxmox::try_block!({
|
||||
if !((data[0] & 0x7f) == 0x12 && data[1] == 0) {
|
||||
bail!("invalid response");
|
||||
}
|
||||
|
||||
let mut reader = &data[2..];
|
||||
|
||||
let page_len: u16 = unsafe { reader.read_be_value()? };
|
||||
if page_len != 0x0c {
|
||||
bail!("invalid page length");
|
||||
}
|
||||
|
||||
let parameter_code: u16 = unsafe { reader.read_be_value()? };
|
||||
if parameter_code != 0 {
|
||||
bail!("invalid parameter code");
|
||||
}
|
||||
|
||||
let mut control_buf = [0u8; 2];
|
||||
reader.read_exact(&mut control_buf)?;
|
||||
|
||||
if control_buf[1] != 8 {
|
||||
bail!("invalid parameter length");
|
||||
}
|
||||
|
||||
let mut value: u64 = unsafe { reader.read_be_value()? };
|
||||
|
||||
// bits are in wrong order, reverse them
|
||||
value = value.reverse_bits();
|
||||
|
||||
Ok(TapeAlertFlags::from_bits_truncate(value))
|
||||
}).map_err(|err| format_err!("decode tape alert flags failed - {}", err))
|
||||
}
|
||||
|
||||
const CRITICAL_FLAG_MASK: u64 =
|
||||
TapeAlertFlags::MEDIA.bits() |
|
||||
TapeAlertFlags::WRITE_FAILURE.bits() |
|
||||
TapeAlertFlags::READ_FAILURE.bits() |
|
||||
TapeAlertFlags::WRITE_PROTECT.bits() |
|
||||
TapeAlertFlags::UNRECOVERABLE_SNAPPED_TAPE.bits() |
|
||||
TapeAlertFlags::FORCED_EJECT.bits() |
|
||||
TapeAlertFlags::EXPIRED_CLEANING_MEDIA.bits() |
|
||||
TapeAlertFlags::INVALID_CLEANING_TAPE.bits() |
|
||||
TapeAlertFlags::HARDWARE_A.bits() |
|
||||
TapeAlertFlags::HARDWARE_B.bits() |
|
||||
TapeAlertFlags::EJECT_MEDIA.bits() |
|
||||
TapeAlertFlags::PREDICTIVE_FAILURE.bits() |
|
||||
TapeAlertFlags::LOADER_STRAY_TAPE.bits() |
|
||||
TapeAlertFlags::LOADER_MAGAZINE.bits() |
|
||||
TapeAlertFlags::TAPE_SYSTEM_AREA_WRITE_FAILURE.bits() |
|
||||
TapeAlertFlags::TAPE_SYSTEM_AREA_READ_FAILURE.bits() |
|
||||
TapeAlertFlags::NO_START_OF_DATA.bits() |
|
||||
TapeAlertFlags::LOADING_FAILURE.bits() |
|
||||
TapeAlertFlags::UNRECOVERABLE_UNLOAD_FAILURE.bits() |
|
||||
TapeAlertFlags::AUTOMATION_INTERFACE_FAILURE.bits();
|
||||
|
||||
/// Check if tape-alert-flags contains critial errors.
|
||||
pub fn tape_alert_flags_critical(flags: TapeAlertFlags) -> bool {
|
||||
(flags.bits() & CRITICAL_FLAG_MASK) != 0
|
||||
}
|
||||
|
||||
const MEDIA_LIFE_MASK: u64 =
|
||||
TapeAlertFlags::MEDIA_LIFE.bits() |
|
||||
TapeAlertFlags::NEARING_MEDIA_LIFE.bits();
|
||||
|
||||
/// Check if tape-alert-flags indicates media-life end
|
||||
pub fn tape_alert_flags_media_life(flags: TapeAlertFlags) -> bool {
|
||||
(flags.bits() & MEDIA_LIFE_MASK) != 0
|
||||
}
|
||||
|
||||
const MEDIA_CLEAN_MASK: u64 =
|
||||
TapeAlertFlags::CLEAN_NOW.bits() |
|
||||
TapeAlertFlags::CLEAN_PERIODIC.bits();
|
||||
|
||||
/// Check if tape-alert-flags indicates media cleaning request
|
||||
pub fn tape_alert_flags_cleaning_request(flags: TapeAlertFlags) -> bool {
|
||||
(flags.bits() & MEDIA_CLEAN_MASK) != 0
|
||||
}
|
230
pbs-tape/src/sg_tape/volume_statistics.rs
Normal file
230
pbs-tape/src/sg_tape/volume_statistics.rs
Normal file
@ -0,0 +1,230 @@
|
||||
use std::io::Read;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use endian_trait::Endian;
|
||||
|
||||
use proxmox::tools::io::ReadExt;
|
||||
|
||||
use pbs_api_types::Lp17VolumeStatistics;
|
||||
|
||||
use crate::sgutils2::SgRaw;
|
||||
|
||||
/// SCSI command to query volume statistics
|
||||
///
|
||||
/// CDB: LOG SENSE / LP17h Volume Statistics
|
||||
///
|
||||
/// The Volume Statistics log page is included in Ultrium 5 and later
|
||||
/// drives.
|
||||
pub fn read_volume_statistics<F: AsRawFd>(file: &mut F) -> Result<Lp17VolumeStatistics, Error> {
|
||||
|
||||
let data = sg_read_volume_statistics(file)?;
|
||||
|
||||
decode_volume_statistics(&data)
|
||||
}
|
||||
|
||||
fn sg_read_volume_statistics<F: AsRawFd>(file: &mut F) -> Result<Vec<u8>, Error> {
|
||||
|
||||
let alloc_len: u16 = 8192;
|
||||
let mut sg_raw = SgRaw::new(file, alloc_len as usize)?;
|
||||
|
||||
let mut cmd = Vec::new();
|
||||
cmd.push(0x4D); // LOG SENSE
|
||||
cmd.push(0);
|
||||
cmd.push((1<<6) | 0x17); // Volume Statistics log page
|
||||
cmd.push(0); // Subpage 0
|
||||
cmd.push(0);
|
||||
cmd.push(0);
|
||||
cmd.push(0);
|
||||
cmd.extend(&alloc_len.to_be_bytes()); // alloc len
|
||||
cmd.push(0u8); // control byte
|
||||
|
||||
sg_raw.do_command(&cmd)
|
||||
.map_err(|err| format_err!("read tape volume statistics failed - {}", err))
|
||||
.map(|v| v.to_vec())
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Endian)]
|
||||
struct LpParameterHeader {
|
||||
parameter_code: u16,
|
||||
control: u8,
|
||||
parameter_len: u8,
|
||||
}
|
||||
|
||||
fn decode_volume_statistics(data: &[u8]) -> Result<Lp17VolumeStatistics, Error> {
|
||||
|
||||
|
||||
let read_be_counter = |reader: &mut &[u8], len: u8| {
|
||||
let len = len as usize;
|
||||
if len == 0 || len > 8 {
|
||||
bail!("invalid conter size '{}'", len);
|
||||
}
|
||||
let mut buffer = [0u8; 8];
|
||||
reader.read_exact(&mut buffer[..len])?;
|
||||
|
||||
let value = buffer
|
||||
.iter()
|
||||
.take(len)
|
||||
.fold(0, |value, curr| (value << 8) | *curr as u64);
|
||||
|
||||
Ok(value)
|
||||
};
|
||||
|
||||
proxmox::try_block!({
|
||||
if !((data[0] & 0x7f) == 0x17 && data[1] == 0) {
|
||||
bail!("invalid response");
|
||||
}
|
||||
|
||||
let mut reader = &data[2..];
|
||||
|
||||
let page_len: u16 = unsafe { reader.read_be_value()? };
|
||||
|
||||
let page_len = page_len as usize;
|
||||
|
||||
if (page_len + 4) > data.len() {
|
||||
bail!("invalid page length");
|
||||
} else {
|
||||
// Note: Quantum hh7 returns the allocation_length instead of real data_len
|
||||
reader = &data[4..page_len+4];
|
||||
}
|
||||
|
||||
let mut stat = Lp17VolumeStatistics::default();
|
||||
let mut page_valid = false;
|
||||
|
||||
loop {
|
||||
if reader.is_empty() {
|
||||
break;
|
||||
}
|
||||
let head: LpParameterHeader = unsafe { reader.read_be_value()? };
|
||||
|
||||
match head.parameter_code {
|
||||
0x0000 => {
|
||||
let value: u64 = read_be_counter(&mut reader, head.parameter_len)?;
|
||||
if value == 0 {
|
||||
bail!("page-valid flag not set");
|
||||
}
|
||||
page_valid = true;
|
||||
}
|
||||
0x0001 => {
|
||||
stat.volume_mounts =
|
||||
read_be_counter(&mut reader, head.parameter_len)?;
|
||||
}
|
||||
0x0002 => {
|
||||
stat.volume_datasets_written =
|
||||
read_be_counter(&mut reader, head.parameter_len)?;
|
||||
}
|
||||
0x0003 => {
|
||||
stat.volume_recovered_write_data_errors =
|
||||
read_be_counter(&mut reader, head.parameter_len)?;
|
||||
}
|
||||
0x0004 => {
|
||||
stat.volume_unrecovered_write_data_errors =
|
||||
read_be_counter(&mut reader, head.parameter_len)?;
|
||||
}
|
||||
0x0005 => {
|
||||
stat.volume_write_servo_errors =
|
||||
read_be_counter(&mut reader, head.parameter_len)?;
|
||||
}
|
||||
0x0006 => {
|
||||
stat.volume_unrecovered_write_servo_errors =
|
||||
read_be_counter(&mut reader, head.parameter_len)?;
|
||||
}
|
||||
0x0007 => {
|
||||
stat.volume_datasets_read =
|
||||
read_be_counter(&mut reader, head.parameter_len)?;
|
||||
}
|
||||
0x0008 => {
|
||||
stat.volume_recovered_read_errors =
|
||||
read_be_counter(&mut reader, head.parameter_len)?;
|
||||
}
|
||||
0x0009 => {
|
||||
stat.volume_unrecovered_read_errors =
|
||||
read_be_counter(&mut reader, head.parameter_len)?;
|
||||
}
|
||||
0x000C => {
|
||||
stat.last_mount_unrecovered_write_errors =
|
||||
read_be_counter(&mut reader, head.parameter_len)?;
|
||||
}
|
||||
0x000D => {
|
||||
stat.last_mount_unrecovered_read_errors =
|
||||
read_be_counter(&mut reader, head.parameter_len)?;
|
||||
}
|
||||
0x000E => {
|
||||
stat.last_mount_bytes_written =
|
||||
read_be_counter(&mut reader, head.parameter_len)? * 1_000_000;
|
||||
}
|
||||
0x000F => {
|
||||
stat.last_mount_bytes_read =
|
||||
read_be_counter(&mut reader, head.parameter_len)? * 1_000_000;
|
||||
}
|
||||
0x0010 => {
|
||||
stat.lifetime_bytes_written =
|
||||
read_be_counter(&mut reader, head.parameter_len)? * 1_000_000;
|
||||
}
|
||||
0x0011 => {
|
||||
stat.lifetime_bytes_read =
|
||||
read_be_counter(&mut reader, head.parameter_len)? * 1_000_000;
|
||||
}
|
||||
0x0012 => {
|
||||
stat.last_load_write_compression_ratio =
|
||||
read_be_counter(&mut reader, head.parameter_len)?;
|
||||
}
|
||||
0x0013 => {
|
||||
stat.last_load_read_compression_ratio =
|
||||
read_be_counter(&mut reader, head.parameter_len)?;
|
||||
}
|
||||
0x0014 => {
|
||||
stat.medium_mount_time =
|
||||
read_be_counter(&mut reader, head.parameter_len)?;
|
||||
}
|
||||
0x0015 => {
|
||||
stat.medium_ready_time =
|
||||
read_be_counter(&mut reader, head.parameter_len)?;
|
||||
}
|
||||
0x0016 => {
|
||||
stat.total_native_capacity =
|
||||
read_be_counter(&mut reader, head.parameter_len)? * 1_000_000;
|
||||
}
|
||||
0x0017 => {
|
||||
stat.total_used_native_capacity =
|
||||
read_be_counter(&mut reader, head.parameter_len)? * 1_000_000;
|
||||
}
|
||||
0x0040 => {
|
||||
let data = reader.read_exact_allocated(head.parameter_len as usize)?;
|
||||
stat.serial = String::from_utf8_lossy(&data).to_string();
|
||||
}
|
||||
0x0080 => {
|
||||
let value = read_be_counter(&mut reader, head.parameter_len)?;
|
||||
if value == 1 {
|
||||
stat.write_protect = true;
|
||||
}
|
||||
}
|
||||
0x0081 => {
|
||||
let value = read_be_counter(&mut reader, head.parameter_len)?;
|
||||
if value == 1 {
|
||||
stat.worm = true;
|
||||
}
|
||||
}
|
||||
0x0101 => {
|
||||
stat.beginning_of_medium_passes =
|
||||
read_be_counter(&mut reader, head.parameter_len)?;
|
||||
}
|
||||
0x0102 => {
|
||||
stat.middle_of_tape_passes =
|
||||
read_be_counter(&mut reader, head.parameter_len)?;
|
||||
}
|
||||
_ => {
|
||||
reader.read_exact_allocated(head.parameter_len as usize)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !page_valid {
|
||||
bail!("missing page-valid parameter");
|
||||
}
|
||||
|
||||
Ok(stat)
|
||||
|
||||
}).map_err(|err| format_err!("decode volume statistics failed - {}", err))
|
||||
}
|
799
pbs-tape/src/sgutils2.rs
Normal file
799
pbs-tape/src/sgutils2.rs
Normal file
@ -0,0 +1,799 @@
|
||||
//! Bindings for libsgutils2
|
||||
//!
|
||||
//! Incomplete, but we currently do not need more.
|
||||
//!
|
||||
//! See: `/usr/include/scsi/sg_pt.h`
|
||||
//!
|
||||
//! The SCSI Commands Reference Manual also contains some useful information.
|
||||
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::ptr::NonNull;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use endian_trait::Endian;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use libc::{c_char, c_int};
|
||||
use std::ffi::CStr;
|
||||
|
||||
use proxmox::tools::io::ReadExt;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub struct SenseInfo {
|
||||
pub sense_key: u8,
|
||||
pub asc: u8,
|
||||
pub ascq: u8,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SenseInfo {
|
||||
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
|
||||
let sense_text = SENSE_KEY_DESCRIPTIONS
|
||||
.get(self.sense_key as usize)
|
||||
.map(|s| String::from(*s))
|
||||
.unwrap_or_else(|| format!("Invalid sense {:02X}", self.sense_key));
|
||||
|
||||
if self.asc == 0 && self.ascq == 0 {
|
||||
write!(f, "{}", sense_text)
|
||||
} else {
|
||||
let additional_sense_text = get_asc_ascq_string(self.asc, self.ascq);
|
||||
write!(f, "{}, {}", sense_text, additional_sense_text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ScsiError {
|
||||
#[error("{0}")]
|
||||
Error(#[from] Error),
|
||||
#[error("{0}")]
|
||||
Sense(#[from] SenseInfo),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for ScsiError {
|
||||
fn from(error: std::io::Error) -> Self {
|
||||
Self::Error(error.into())
|
||||
}
|
||||
}
|
||||
|
||||
// Opaque wrapper for sg_pt_base
|
||||
#[repr(C)]
|
||||
struct SgPtBase { _private: [u8; 0] }
|
||||
|
||||
#[repr(transparent)]
|
||||
struct SgPt {
|
||||
raw: NonNull<SgPtBase>,
|
||||
}
|
||||
|
||||
impl Drop for SgPt {
|
||||
fn drop(&mut self) {
|
||||
unsafe { destruct_scsi_pt_obj(self.as_mut_ptr()) };
|
||||
}
|
||||
}
|
||||
|
||||
impl SgPt {
|
||||
fn new() -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
raw: NonNull::new(unsafe { construct_scsi_pt_obj() })
|
||||
.ok_or_else(|| format_err!("construct_scsi_pt_ob failed"))?,
|
||||
})
|
||||
}
|
||||
|
||||
fn as_ptr(&self) -> *const SgPtBase {
|
||||
self.raw.as_ptr()
|
||||
}
|
||||
|
||||
fn as_mut_ptr(&mut self) -> *mut SgPtBase {
|
||||
self.raw.as_ptr()
|
||||
}
|
||||
}
|
||||
|
||||
/// Peripheral device type text (see `inquiry` command)
|
||||
///
|
||||
/// see [https://en.wikipedia.org/wiki/SCSI_Peripheral_Device_Type]
|
||||
pub const PERIPHERAL_DEVICE_TYPE_TEXT: [&'static str; 32] = [
|
||||
"Disk Drive",
|
||||
"Tape Drive",
|
||||
"Printer",
|
||||
"Processor",
|
||||
"Write-once",
|
||||
"CD-ROM", // 05h
|
||||
"Scanner",
|
||||
"Optical",
|
||||
"Medium Changer", // 08h
|
||||
"Communications",
|
||||
"ASC IT8",
|
||||
"ASC IT8",
|
||||
"RAID Array",
|
||||
"Enclosure Services",
|
||||
"Simplified direct-access",
|
||||
"Optical card reader/writer",
|
||||
"Bridging Expander",
|
||||
"Object-based Storage",
|
||||
"Automation/Drive Interface",
|
||||
"Security manager",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Unknown",
|
||||
];
|
||||
|
||||
// SENSE KEYS
|
||||
pub const SENSE_KEY_NO_SENSE: u8 = 0x00;
|
||||
pub const SENSE_KEY_RECOVERED_ERROR: u8 = 0x01;
|
||||
pub const SENSE_KEY_NOT_READY: u8 = 0x02;
|
||||
pub const SENSE_KEY_MEDIUM_ERROR: u8 = 0x03;
|
||||
pub const SENSE_KEY_HARDWARE_ERROR: u8 = 0x04;
|
||||
pub const SENSE_KEY_ILLEGAL_REQUEST: u8 = 0x05;
|
||||
pub const SENSE_KEY_UNIT_ATTENTION: u8 = 0x06;
|
||||
pub const SENSE_KEY_DATA_PROTECT: u8 = 0x07;
|
||||
pub const SENSE_KEY_BLANK_CHECK: u8 = 0x08;
|
||||
pub const SENSE_KEY_COPY_ABORTED: u8 = 0x0a;
|
||||
pub const SENSE_KEY_ABORTED_COMMAND: u8 = 0x0b;
|
||||
pub const SENSE_KEY_VOLUME_OVERFLOW: u8 = 0x0d;
|
||||
pub const SENSE_KEY_MISCOMPARE: u8 = 0x0e;
|
||||
|
||||
/// Sense Key Descriptions
|
||||
pub const SENSE_KEY_DESCRIPTIONS: [&'static str; 16] = [
|
||||
"No Sense",
|
||||
"Recovered Error",
|
||||
"Not Ready",
|
||||
"Medium Error",
|
||||
"Hardware Error",
|
||||
"Illegal Request",
|
||||
"Unit Attention",
|
||||
"Data Protect",
|
||||
"Blank Check",
|
||||
"Vendor specific",
|
||||
"Copy Aborted",
|
||||
"Aborted Command",
|
||||
"Equal",
|
||||
"Volume Overflow",
|
||||
"Miscompare",
|
||||
"Completed",
|
||||
];
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Endian)]
|
||||
// Standard Inquiry page - 36 bytes
|
||||
struct InquiryPage {
|
||||
peripheral_type: u8,
|
||||
rmb: u8,
|
||||
version: u8,
|
||||
flags3: u8,
|
||||
additional_length: u8,
|
||||
flags5: u8,
|
||||
flags6: u8,
|
||||
flags7: u8,
|
||||
vendor: [u8; 8],
|
||||
product: [u8; 16],
|
||||
revision: [u8; 4],
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Endian, Debug)]
|
||||
pub struct RequestSenseFixed {
|
||||
pub response_code: u8,
|
||||
obsolete: u8,
|
||||
pub flags2: u8,
|
||||
pub information: [u8;4],
|
||||
pub additional_sense_len: u8,
|
||||
pub command_specific_information: [u8;4],
|
||||
pub additional_sense_code: u8,
|
||||
pub additional_sense_code_qualifier: u8,
|
||||
pub field_replacable_unit_code: u8,
|
||||
pub sense_key_specific: [u8; 3],
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Endian, Debug)]
|
||||
struct RequestSenseDescriptor{
|
||||
response_code: u8,
|
||||
sense_key: u8,
|
||||
additional_sense_code: u8,
|
||||
additional_sense_code_qualifier: u8,
|
||||
reserved: [u8;4],
|
||||
additional_sense_len: u8,
|
||||
}
|
||||
|
||||
/// Inquiry result
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct InquiryInfo {
|
||||
/// Peripheral device type (0-31)
|
||||
pub peripheral_type: u8,
|
||||
/// Peripheral device type as string
|
||||
pub peripheral_type_text: String,
|
||||
/// Vendor
|
||||
pub vendor: String,
|
||||
/// Product
|
||||
pub product: String,
|
||||
/// Revision
|
||||
pub revision: String,
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Endian, Debug, Copy, Clone)]
|
||||
pub struct ModeParameterHeader {
|
||||
pub mode_data_len: u16,
|
||||
// Note: medium_type and density_code are not the same. On HP
|
||||
// drives, medium_type provides very limited information and is
|
||||
// not compatible with IBM.
|
||||
pub medium_type: u8,
|
||||
pub flags3: u8,
|
||||
reserved4: [u8;2],
|
||||
pub block_descriptior_len: u16,
|
||||
}
|
||||
|
||||
impl ModeParameterHeader {
|
||||
|
||||
pub fn buffer_mode(&self) -> u8 {
|
||||
(self.flags3 & 0b0111_0000) >> 4
|
||||
}
|
||||
|
||||
pub fn set_buffer_mode(&mut self, buffer_mode: bool) {
|
||||
let mut mode = self.flags3 & 0b1_000_1111;
|
||||
if buffer_mode {
|
||||
mode |= 0b0_001_0000;
|
||||
}
|
||||
self.flags3 = mode;
|
||||
}
|
||||
|
||||
pub fn write_protect(&self) -> bool {
|
||||
(self.flags3 & 0b1000_0000) != 0
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Endian, Debug, Copy, Clone)]
|
||||
/// SCSI ModeBlockDescriptor for Tape devices
|
||||
pub struct ModeBlockDescriptor {
|
||||
pub density_code: u8,
|
||||
pub number_of_blocks: [u8;3],
|
||||
reserverd: u8,
|
||||
pub block_length: [u8; 3],
|
||||
}
|
||||
|
||||
impl ModeBlockDescriptor {
|
||||
|
||||
pub fn block_length(&self) -> u32 {
|
||||
((self.block_length[0] as u32) << 16) +
|
||||
((self.block_length[1] as u32) << 8) +
|
||||
(self.block_length[2] as u32)
|
||||
|
||||
}
|
||||
|
||||
pub fn set_block_length(&mut self, length: u32) -> Result<(), Error> {
|
||||
if length > 0x80_00_00 {
|
||||
bail!("block length '{}' is too large", length);
|
||||
}
|
||||
self.block_length[0] = ((length & 0x00ff0000) >> 16) as u8;
|
||||
self.block_length[1] = ((length & 0x0000ff00) >> 8) as u8;
|
||||
self.block_length[2] = (length & 0x000000ff) as u8;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub const SCSI_PT_DO_START_OK:c_int = 0;
|
||||
pub const SCSI_PT_DO_BAD_PARAMS:c_int = 1;
|
||||
pub const SCSI_PT_DO_TIMEOUT:c_int = 2;
|
||||
|
||||
pub const SCSI_PT_RESULT_GOOD:c_int = 0;
|
||||
pub const SCSI_PT_RESULT_STATUS:c_int = 1;
|
||||
pub const SCSI_PT_RESULT_SENSE:c_int = 2;
|
||||
pub const SCSI_PT_RESULT_TRANSPORT_ERR:c_int = 3;
|
||||
pub const SCSI_PT_RESULT_OS_ERR:c_int = 4;
|
||||
|
||||
#[link(name = "sgutils2")]
|
||||
extern "C" {
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn scsi_pt_open_device(
|
||||
device_name: * const c_char,
|
||||
read_only: bool,
|
||||
verbose: c_int,
|
||||
) -> c_int;
|
||||
|
||||
fn sg_is_scsi_cdb(
|
||||
cdbp: *const u8,
|
||||
clen: c_int,
|
||||
) -> bool;
|
||||
|
||||
fn construct_scsi_pt_obj() -> *mut SgPtBase;
|
||||
fn destruct_scsi_pt_obj(objp: *mut SgPtBase);
|
||||
|
||||
fn set_scsi_pt_data_in(
|
||||
objp: *mut SgPtBase,
|
||||
dxferp: *mut u8,
|
||||
dxfer_ilen: c_int,
|
||||
);
|
||||
|
||||
fn set_scsi_pt_data_out(
|
||||
objp: *mut SgPtBase,
|
||||
dxferp: *const u8,
|
||||
dxfer_olen: c_int,
|
||||
);
|
||||
|
||||
fn set_scsi_pt_cdb(
|
||||
objp: *mut SgPtBase,
|
||||
cdb: *const u8,
|
||||
cdb_len: c_int,
|
||||
);
|
||||
|
||||
fn set_scsi_pt_sense(
|
||||
objp: *mut SgPtBase,
|
||||
sense: *mut u8,
|
||||
max_sense_len: c_int,
|
||||
);
|
||||
|
||||
fn do_scsi_pt(
|
||||
objp: *mut SgPtBase,
|
||||
fd: c_int,
|
||||
timeout_secs: c_int,
|
||||
verbose: c_int,
|
||||
) -> c_int;
|
||||
|
||||
fn get_scsi_pt_resid(objp: *const SgPtBase) -> c_int;
|
||||
|
||||
fn get_scsi_pt_sense_len(objp: *const SgPtBase) -> c_int;
|
||||
|
||||
fn get_scsi_pt_status_response(objp: *const SgPtBase) -> c_int;
|
||||
|
||||
fn get_scsi_pt_result_category(objp: *const SgPtBase) -> c_int;
|
||||
|
||||
fn get_scsi_pt_os_err(objp: *const SgPtBase) -> c_int;
|
||||
|
||||
fn sg_get_asc_ascq_str(
|
||||
asc: c_int,
|
||||
ascq:c_int,
|
||||
buff_len: c_int,
|
||||
buffer: *mut c_char,
|
||||
) -> * const c_char;
|
||||
}
|
||||
|
||||
/// Safe interface to run RAW SCSI commands
|
||||
pub struct SgRaw<'a, F> {
|
||||
file: &'a mut F,
|
||||
buffer: Box<[u8]>,
|
||||
sense_buffer: [u8; 32],
|
||||
timeout: i32,
|
||||
}
|
||||
|
||||
/// Get the string associated with ASC/ASCQ values
|
||||
pub fn get_asc_ascq_string(asc: u8, ascq: u8) -> String {
|
||||
|
||||
let mut buffer = [0u8; 1024];
|
||||
let res = unsafe {
|
||||
sg_get_asc_ascq_str(
|
||||
asc as c_int,
|
||||
ascq as c_int,
|
||||
buffer.len() as c_int,
|
||||
buffer.as_mut_ptr() as * mut c_char,
|
||||
)
|
||||
};
|
||||
|
||||
proxmox::try_block!({
|
||||
if res.is_null() { // just to be safe
|
||||
bail!("unexpected NULL ptr");
|
||||
}
|
||||
Ok(unsafe { CStr::from_ptr(res) }.to_str()?.to_owned())
|
||||
}).unwrap_or_else(|_err: Error| {
|
||||
format!("ASC={:02x}x, ASCQ={:02x}x", asc, ascq)
|
||||
})
|
||||
}
|
||||
|
||||
/// Allocate a page aligned buffer
|
||||
///
|
||||
/// SG RAWIO commands needs page aligned transfer buffers.
|
||||
pub fn alloc_page_aligned_buffer(buffer_size: usize) -> Result<Box<[u8]> , Error> {
|
||||
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
|
||||
let layout = std::alloc::Layout::from_size_align(buffer_size, page_size)?;
|
||||
let dinp = unsafe { std::alloc::alloc_zeroed(layout) };
|
||||
if dinp.is_null() {
|
||||
bail!("alloc SCSI output buffer failed");
|
||||
}
|
||||
|
||||
let buffer_slice = unsafe { std::slice::from_raw_parts_mut(dinp, buffer_size)};
|
||||
Ok(unsafe { Box::from_raw(buffer_slice) })
|
||||
}
|
||||
|
||||
impl <'a, F: AsRawFd> SgRaw<'a, F> {
|
||||
|
||||
/// Create a new instance to run commands
|
||||
///
|
||||
/// The file must be a handle to a SCSI device.
|
||||
pub fn new(file: &'a mut F, buffer_size: usize) -> Result<Self, Error> {
|
||||
|
||||
let buffer;
|
||||
|
||||
if buffer_size > 0 {
|
||||
buffer = alloc_page_aligned_buffer(buffer_size)?;
|
||||
} else {
|
||||
buffer = Box::new([]);
|
||||
}
|
||||
|
||||
let sense_buffer = [0u8; 32];
|
||||
|
||||
Ok(Self { file, buffer, sense_buffer, timeout: 0 })
|
||||
}
|
||||
|
||||
/// Set the command timeout in seconds (0 means default (60 seconds))
|
||||
pub fn set_timeout(&mut self, seconds: usize) {
|
||||
if seconds > (i32::MAX as usize) {
|
||||
self.timeout = i32::MAX; // don't care about larger values
|
||||
} else {
|
||||
self.timeout = seconds as i32;
|
||||
}
|
||||
}
|
||||
|
||||
// create new object with initialized data_in and sense buffer
|
||||
fn create_scsi_pt_obj(&mut self) -> Result<SgPt, Error> {
|
||||
|
||||
let mut ptvp = SgPt::new()?;
|
||||
|
||||
if self.buffer.len() > 0 {
|
||||
unsafe {
|
||||
set_scsi_pt_data_in(
|
||||
ptvp.as_mut_ptr(),
|
||||
self.buffer.as_mut_ptr(),
|
||||
self.buffer.len() as c_int,
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
unsafe {
|
||||
set_scsi_pt_sense(
|
||||
ptvp.as_mut_ptr(),
|
||||
self.sense_buffer.as_mut_ptr(),
|
||||
self.sense_buffer.len() as c_int,
|
||||
)
|
||||
};
|
||||
|
||||
Ok(ptvp)
|
||||
}
|
||||
|
||||
fn do_scsi_pt_checked(&mut self, ptvp: &mut SgPt) -> Result<(), ScsiError> {
|
||||
|
||||
let res = unsafe { do_scsi_pt(ptvp.as_mut_ptr(), self.file.as_raw_fd(), self.timeout, 0) };
|
||||
match res {
|
||||
SCSI_PT_DO_START_OK => { /* Ok */ },
|
||||
SCSI_PT_DO_BAD_PARAMS => return Err(format_err!("do_scsi_pt failed - bad pass through setup").into()),
|
||||
SCSI_PT_DO_TIMEOUT => return Err(format_err!("do_scsi_pt failed - timeout").into()),
|
||||
code if code < 0 => {
|
||||
let errno = unsafe { get_scsi_pt_os_err(ptvp.as_ptr()) };
|
||||
let err = nix::Error::from_errno(nix::errno::Errno::from_i32(errno));
|
||||
return Err(format_err!("do_scsi_pt failed with err {}", err).into());
|
||||
}
|
||||
unknown => return Err(format_err!("do_scsi_pt failed: unknown error {}", unknown).into()),
|
||||
}
|
||||
|
||||
if res < 0 {
|
||||
let err = nix::Error::last();
|
||||
return Err(format_err!("do_scsi_pt failed - {}", err).into());
|
||||
}
|
||||
if res != 0 {
|
||||
return Err(format_err!("do_scsi_pt failed {}", res).into());
|
||||
}
|
||||
|
||||
let sense_len = unsafe { get_scsi_pt_sense_len(ptvp.as_ptr()) };
|
||||
|
||||
let res_cat = unsafe { get_scsi_pt_result_category(ptvp.as_ptr()) };
|
||||
match res_cat {
|
||||
SCSI_PT_RESULT_GOOD => return Ok(()),
|
||||
SCSI_PT_RESULT_STATUS => {
|
||||
let status = unsafe { get_scsi_pt_status_response(ptvp.as_ptr()) };
|
||||
if status != 0 {
|
||||
return Err(format_err!("unknown scsi error - status response {}", status).into());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
SCSI_PT_RESULT_SENSE => {
|
||||
if sense_len == 0 {
|
||||
return Err(format_err!("scsi command failed, but got no sense data").into());
|
||||
}
|
||||
|
||||
let code = self.sense_buffer[0] & 0x7f;
|
||||
|
||||
let mut reader = &self.sense_buffer[..(sense_len as usize)];
|
||||
|
||||
let sense = match code {
|
||||
0x70 => {
|
||||
let sense: RequestSenseFixed = unsafe { reader.read_be_value()? };
|
||||
SenseInfo {
|
||||
sense_key: sense.flags2 & 0xf,
|
||||
asc: sense.additional_sense_code,
|
||||
ascq: sense.additional_sense_code_qualifier,
|
||||
}
|
||||
}
|
||||
0x72 => {
|
||||
let sense: RequestSenseDescriptor = unsafe { reader.read_be_value()? };
|
||||
SenseInfo {
|
||||
sense_key: sense.sense_key & 0xf,
|
||||
asc: sense.additional_sense_code,
|
||||
ascq: sense.additional_sense_code_qualifier,
|
||||
}
|
||||
}
|
||||
0x71 | 0x73 => {
|
||||
return Err(format_err!("scsi command failed: received deferred Sense").into());
|
||||
}
|
||||
unknown => {
|
||||
return Err(format_err!("scsi command failed: invalid Sense response code {:x}", unknown).into());
|
||||
}
|
||||
};
|
||||
|
||||
return Err(ScsiError::Sense(sense));
|
||||
}
|
||||
SCSI_PT_RESULT_TRANSPORT_ERR => return Err(format_err!("scsi command failed: transport error").into()),
|
||||
SCSI_PT_RESULT_OS_ERR => {
|
||||
let errno = unsafe { get_scsi_pt_os_err(ptvp.as_ptr()) };
|
||||
let err = nix::Error::from_errno(nix::errno::Errno::from_i32(errno));
|
||||
return Err(format_err!("scsi command failed with err {}", err).into());
|
||||
}
|
||||
unknown => return Err(format_err!("scsi command failed: unknown result category {}", unknown).into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the specified RAW SCSI command
|
||||
pub fn do_command(&mut self, cmd: &[u8]) -> Result<&[u8], ScsiError> {
|
||||
|
||||
if !unsafe { sg_is_scsi_cdb(cmd.as_ptr(), cmd.len() as c_int) } {
|
||||
return Err(format_err!("no valid SCSI command").into());
|
||||
}
|
||||
|
||||
if self.buffer.len() < 16 {
|
||||
return Err(format_err!("input buffer too small").into());
|
||||
}
|
||||
|
||||
let mut ptvp = self.create_scsi_pt_obj()?;
|
||||
|
||||
unsafe {
|
||||
set_scsi_pt_cdb(
|
||||
ptvp.as_mut_ptr(),
|
||||
cmd.as_ptr(),
|
||||
cmd.len() as c_int,
|
||||
)
|
||||
};
|
||||
|
||||
self.do_scsi_pt_checked(&mut ptvp)?;
|
||||
|
||||
let resid = unsafe { get_scsi_pt_resid(ptvp.as_ptr()) } as usize;
|
||||
if resid > self.buffer.len() {
|
||||
return Err(format_err!("do_scsi_pt failed - got strange resid (value too big)").into());
|
||||
}
|
||||
let data_len = self.buffer.len() - resid;
|
||||
|
||||
Ok(&self.buffer[..data_len])
|
||||
}
|
||||
|
||||
/// Run the specified RAW SCSI command, use data as input buffer
|
||||
pub fn do_in_command<'b>(&mut self, cmd: &[u8], data: &'b mut [u8]) -> Result<&'b [u8], ScsiError> {
|
||||
|
||||
if !unsafe { sg_is_scsi_cdb(cmd.as_ptr(), cmd.len() as c_int) } {
|
||||
return Err(format_err!("no valid SCSI command").into());
|
||||
}
|
||||
|
||||
if data.len() == 0 {
|
||||
return Err(format_err!("got zero-sized input buffer").into());
|
||||
}
|
||||
|
||||
let mut ptvp = self.create_scsi_pt_obj()?;
|
||||
|
||||
unsafe {
|
||||
set_scsi_pt_data_in(
|
||||
ptvp.as_mut_ptr(),
|
||||
data.as_mut_ptr(),
|
||||
data.len() as c_int,
|
||||
);
|
||||
|
||||
set_scsi_pt_cdb(
|
||||
ptvp.as_mut_ptr(),
|
||||
cmd.as_ptr(),
|
||||
cmd.len() as c_int,
|
||||
);
|
||||
};
|
||||
|
||||
self.do_scsi_pt_checked(&mut ptvp)?;
|
||||
|
||||
let resid = unsafe { get_scsi_pt_resid(ptvp.as_ptr()) } as usize;
|
||||
|
||||
if resid > data.len() {
|
||||
return Err(format_err!("do_scsi_pt failed - got strange resid (value too big)").into());
|
||||
}
|
||||
let data_len = data.len() - resid;
|
||||
|
||||
Ok(&data[..data_len])
|
||||
}
|
||||
|
||||
/// Run dataout command
|
||||
///
|
||||
/// Note: use alloc_page_aligned_buffer to alloc data transfer buffer
|
||||
pub fn do_out_command(&mut self, cmd: &[u8], data: &[u8]) -> Result<(), ScsiError> {
|
||||
|
||||
if !unsafe { sg_is_scsi_cdb(cmd.as_ptr(), cmd.len() as c_int) } {
|
||||
return Err(format_err!("no valid SCSI command").into());
|
||||
}
|
||||
|
||||
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
|
||||
if ((data.as_ptr() as usize) & (page_size -1)) != 0 {
|
||||
return Err(format_err!("wrong transfer buffer alignment").into());
|
||||
}
|
||||
|
||||
let mut ptvp = self.create_scsi_pt_obj()?;
|
||||
|
||||
unsafe {
|
||||
set_scsi_pt_data_out(
|
||||
ptvp.as_mut_ptr(),
|
||||
data.as_ptr(),
|
||||
data.len() as c_int,
|
||||
);
|
||||
|
||||
set_scsi_pt_cdb(
|
||||
ptvp.as_mut_ptr(),
|
||||
cmd.as_ptr(),
|
||||
cmd.len() as c_int,
|
||||
);
|
||||
};
|
||||
|
||||
self.do_scsi_pt_checked(&mut ptvp)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Useful helpers
|
||||
|
||||
/// Converts SCSI ASCII text into String, trim zero and spaces
|
||||
pub fn scsi_ascii_to_string(data: &[u8]) -> String {
|
||||
String::from_utf8_lossy(data)
|
||||
.trim_matches(char::from(0))
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Read SCSI Inquiry page
|
||||
///
|
||||
/// Returns Product/Vendor/Revision and device type.
|
||||
pub fn scsi_inquiry<F: AsRawFd>(
|
||||
file: &mut F,
|
||||
) -> Result<InquiryInfo, Error> {
|
||||
|
||||
let allocation_len: u8 = std::mem::size_of::<InquiryPage>() as u8;
|
||||
|
||||
let mut sg_raw = SgRaw::new(file, allocation_len as usize)?;
|
||||
sg_raw.set_timeout(30); // use short timeout
|
||||
|
||||
let mut cmd = Vec::new();
|
||||
cmd.extend(&[0x12, 0, 0, 0, allocation_len, 0]); // INQUIRY
|
||||
|
||||
let data = sg_raw.do_command(&cmd)
|
||||
.map_err(|err| format_err!("SCSI inquiry failed - {}", err))?;
|
||||
|
||||
proxmox::try_block!({
|
||||
let mut reader = &data[..];
|
||||
|
||||
let page: InquiryPage = unsafe { reader.read_be_value()? };
|
||||
|
||||
let peripheral_type = page.peripheral_type & 31;
|
||||
|
||||
let info = InquiryInfo {
|
||||
peripheral_type,
|
||||
peripheral_type_text: PERIPHERAL_DEVICE_TYPE_TEXT[peripheral_type as usize].to_string(),
|
||||
vendor: scsi_ascii_to_string(&page.vendor),
|
||||
product: scsi_ascii_to_string(&page.product),
|
||||
revision: scsi_ascii_to_string(&page.revision),
|
||||
};
|
||||
|
||||
Ok(info)
|
||||
}).map_err(|err: Error| format_err!("decode inquiry page failed - {}", err))
|
||||
}
|
||||
|
||||
/// Run SCSI Mode Sense
|
||||
///
|
||||
/// Warning: P needs to be repr(C, packed)]
|
||||
pub fn scsi_mode_sense<F: AsRawFd, P: Endian>(
|
||||
file: &mut F,
|
||||
disable_block_descriptor: bool,
|
||||
page_code: u8,
|
||||
sub_page_code: u8,
|
||||
) -> Result<(ModeParameterHeader, Option<ModeBlockDescriptor>, P), Error> {
|
||||
|
||||
let allocation_len: u16 = 4096;
|
||||
let mut sg_raw = SgRaw::new(file, allocation_len as usize)?;
|
||||
|
||||
let mut cmd = Vec::new();
|
||||
cmd.push(0x5A); // MODE SENSE(10)
|
||||
if disable_block_descriptor {
|
||||
cmd.push(8); // DBD=1 (Disable Block Descriptors)
|
||||
} else {
|
||||
cmd.push(0); // DBD=0 (Include Block Descriptors)
|
||||
}
|
||||
cmd.push(page_code & 63); // report current values for page_code
|
||||
cmd.push(sub_page_code);
|
||||
|
||||
cmd.extend(&[0, 0, 0]); // reserved
|
||||
cmd.extend(&allocation_len.to_be_bytes()); // allocation len
|
||||
cmd.push(0); //control
|
||||
|
||||
let data = sg_raw.do_command(&cmd)
|
||||
.map_err(|err| format_err!("mode sense failed - {}", err))?;
|
||||
|
||||
proxmox::try_block!({
|
||||
let mut reader = &data[..];
|
||||
|
||||
let head: ModeParameterHeader = unsafe { reader.read_be_value()? };
|
||||
let expected_len = head.mode_data_len as usize + 2;
|
||||
|
||||
if data.len() < expected_len {
|
||||
bail!("wrong mode_data_len: got {}, expected {}", data.len(), expected_len);
|
||||
} else if data.len() > expected_len {
|
||||
// Note: Some hh7 drives returns the allocation_length
|
||||
// instead of real data_len
|
||||
let header_size = std::mem::size_of::<ModeParameterHeader>();
|
||||
reader = &data[header_size..expected_len];
|
||||
}
|
||||
|
||||
if disable_block_descriptor && head.block_descriptior_len != 0 {
|
||||
let len = head.block_descriptior_len;
|
||||
bail!("wrong block_descriptior_len: {}, expected 0", len);
|
||||
}
|
||||
|
||||
let mut block_descriptor: Option<ModeBlockDescriptor> = None;
|
||||
|
||||
if !disable_block_descriptor {
|
||||
if head.block_descriptior_len != 8 {
|
||||
let len = head.block_descriptior_len;
|
||||
bail!("wrong block_descriptior_len: {}, expected 8", len);
|
||||
}
|
||||
|
||||
block_descriptor = Some(unsafe { reader.read_be_value()? });
|
||||
}
|
||||
|
||||
let page: P = unsafe { reader.read_be_value()? };
|
||||
|
||||
Ok((head, block_descriptor, page))
|
||||
}).map_err(|err: Error| format_err!("decode mode sense failed - {}", err))
|
||||
}
|
||||
|
||||
/// Resuqest Sense
|
||||
pub fn scsi_request_sense<F: AsRawFd>(
|
||||
file: &mut F,
|
||||
) -> Result<RequestSenseFixed, ScsiError> {
|
||||
|
||||
// request 252 bytes, as mentioned in the Seagate SCSI reference
|
||||
let allocation_len: u8 = 252;
|
||||
|
||||
let mut sg_raw = SgRaw::new(file, allocation_len as usize)?;
|
||||
sg_raw.set_timeout(30); // use short timeout
|
||||
let mut cmd = Vec::new();
|
||||
cmd.extend(&[0x03, 0, 0, 0, allocation_len, 0]); // REQUEST SENSE FIXED FORMAT
|
||||
|
||||
let data = sg_raw.do_command(&cmd)
|
||||
.map_err(|err| format_err!("request sense failed - {}", err))?;
|
||||
|
||||
let sense = proxmox::try_block!({
|
||||
let data_len = data.len();
|
||||
|
||||
if data_len < std::mem::size_of::<RequestSenseFixed>() {
|
||||
bail!("got short data len ({})", data_len);
|
||||
}
|
||||
let code = data[0] & 0x7f;
|
||||
if code != 0x70 {
|
||||
bail!("received unexpected sense code '0x{:02x}'", code);
|
||||
}
|
||||
|
||||
let mut reader = &data[..];
|
||||
|
||||
let sense: RequestSenseFixed = unsafe { reader.read_be_value()? };
|
||||
|
||||
Ok(sense)
|
||||
}).map_err(|err: Error| format_err!("decode request sense failed - {}", err))?;
|
||||
|
||||
Ok(sense)
|
||||
}
|
38
pbs-tape/src/tape_read.rs
Normal file
38
pbs-tape/src/tape_read.rs
Normal file
@ -0,0 +1,38 @@
|
||||
use std::io::Read;
|
||||
|
||||
/// Read trait for tape devices
|
||||
///
|
||||
/// Normal Read, but allows to query additional status flags.
|
||||
pub trait TapeRead: Read {
|
||||
/// Return true if there is an "INCOMPLETE" mark at EOF
|
||||
///
|
||||
/// Raises an error if you query this flag before reaching EOF.
|
||||
fn is_incomplete(&self) -> Result<bool, std::io::Error>;
|
||||
|
||||
/// Return true if there is a file end marker before EOF
|
||||
///
|
||||
/// Raises an error if you query this flag before reaching EOF.
|
||||
fn has_end_marker(&self) -> Result<bool, std::io::Error>;
|
||||
|
||||
/// Skip data by reading to EOF (position after EOF marker)
|
||||
///
|
||||
// Returns the number of bytes skipped. This does not raise an
|
||||
// error if the stream has no end marker.
|
||||
fn skip_data(&mut self) -> Result<usize, std::io::Error>;
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum BlockReadError {
|
||||
#[error("{0}")]
|
||||
Error(#[from] std::io::Error),
|
||||
#[error("end of file")]
|
||||
EndOfFile,
|
||||
#[error("end of data stream")]
|
||||
EndOfStream,
|
||||
}
|
||||
|
||||
/// Read streams of blocks
|
||||
pub trait BlockRead {
|
||||
/// Read the next block (whole buffer)
|
||||
fn read_block(&mut self, buffer: &mut [u8]) -> Result<usize, BlockReadError>;
|
||||
}
|
62
pbs-tape/src/tape_write.rs
Normal file
62
pbs-tape/src/tape_write.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use endian_trait::Endian;
|
||||
|
||||
use crate::MediaContentHeader;
|
||||
|
||||
/// Write trait for tape devices
|
||||
///
|
||||
/// The 'write_all' function returns if the drive reached the Logical
|
||||
/// End Of Media (early warning).
|
||||
///
|
||||
/// It is mandatory to call 'finish' before closing the stream to mark it
|
||||
/// as correctly written.
|
||||
///
|
||||
/// Please note that there is no flush method. Tapes flush there internal
|
||||
/// buffer when they write an EOF marker.
|
||||
pub trait TapeWrite {
|
||||
/// writes all data, returns true on LEOM
|
||||
fn write_all(&mut self, data: &[u8]) -> Result<bool, std::io::Error>;
|
||||
|
||||
/// Returns how many bytes (raw data on tape) have been written
|
||||
fn bytes_written(&self) -> usize;
|
||||
|
||||
/// flush last block, write file end mark
|
||||
///
|
||||
/// The incomplete flag is used to mark multivolume stream.
|
||||
fn finish(&mut self, incomplete: bool) -> Result<bool, std::io::Error>;
|
||||
|
||||
/// Returns true if the writer already detected the logical end of media
|
||||
fn logical_end_of_media(&self) -> bool;
|
||||
|
||||
/// writes header and data, returns true on LEOM
|
||||
fn write_header(
|
||||
&mut self,
|
||||
header: &MediaContentHeader,
|
||||
data: &[u8],
|
||||
) -> Result<bool, std::io::Error> {
|
||||
if header.size as usize != data.len() {
|
||||
proxmox::io_bail!("write_header with wrong size - internal error");
|
||||
}
|
||||
let header = header.to_le();
|
||||
|
||||
let res = self.write_all(unsafe { std::slice::from_raw_parts(
|
||||
&header as *const MediaContentHeader as *const u8,
|
||||
std::mem::size_of::<MediaContentHeader>(),
|
||||
)})?;
|
||||
|
||||
if data.is_empty() { return Ok(res); }
|
||||
|
||||
self.write_all(data)
|
||||
}
|
||||
}
|
||||
|
||||
/// Write streams of blocks
|
||||
pub trait BlockWrite {
|
||||
/// Write a data block
|
||||
///
|
||||
/// Returns true if the drive reached the Logical End Of Media
|
||||
/// (early warning)
|
||||
fn write_block(&mut self, buffer: &[u8]) -> Result<bool, std::io::Error>;
|
||||
|
||||
/// Write a filemark
|
||||
fn write_filemark(&mut self) -> Result<(), std::io::Error>;
|
||||
}
|
Reference in New Issue
Block a user