diff --git a/src/bin/sg-tape-cmd.rs b/src/bin/sg-tape-cmd.rs index dcc26f6d..7a16642d 100644 --- a/src/bin/sg-tape-cmd.rs +++ b/src/bin/sg-tape-cmd.rs @@ -122,6 +122,31 @@ fn tape_alert_flags( Ok(()) } +#[api( + input: { + properties: { + device: { + schema: LINUX_DRIVE_PATH_SCHEMA, + optional: true, + }, + }, + }, +)] +/// Read volume statistics +fn volume_statistics( + device: Option, +) -> Result<(), Error> { + + let result = proxmox::try_block!({ + let mut handle = get_tape_handle(device)?; + handle.volume_statistics() + }).map_err(|err: Error| err.to_string()); + + println!("{}", serde_json::to_string_pretty(&result)?); + + Ok(()) +} + fn main() -> Result<(), Error> { // check if we are user root or backup @@ -157,6 +182,10 @@ fn main() -> Result<(), Error> { "tape-alert-flags", CliCommand::new(&API_METHOD_TAPE_ALERT_FLAGS) ) + .insert( + "volume-statistics", + CliCommand::new(&API_METHOD_VOLUME_STATISTICS) + ) ; let mut rpcenv = CliEnvironment::new(); diff --git a/src/tape/drive/linux_tape.rs b/src/tape/drive/linux_tape.rs index b2e3d69e..1e2187e3 100644 --- a/src/tape/drive/linux_tape.rs +++ b/src/tape/drive/linux_tape.rs @@ -19,9 +19,11 @@ use crate::{ TapeRead, TapeWrite, TapeAlertFlags, + Lp17VolumeStatistics, read_mam_attributes, mam_extract_media_usage, read_tape_alert_flags, + read_volume_statistics, drive::{ LinuxTapeDrive, TapeDriver, @@ -323,6 +325,25 @@ impl LinuxTapeHandle { .map_err(|err| format_err!("{}", err)) .map(|bits| TapeAlertFlags::from_bits_truncate(bits)) } + + /// Read Volume Statistics + /// + /// Note: Only 'root' user may run RAW SG commands, so we need to + /// spawn setuid binary 'sg-tape-cmd'. + pub fn volume_statistics(&mut self) -> Result { + + if nix::unistd::Uid::effective().is_root() { + return read_volume_statistics(&mut self.file); + } + + let mut command = std::process::Command::new( + "/usr/lib/x86_64-linux-gnu/proxmox-backup/sg-tape-cmd"); + command.args(&["volume-statistics"]); + command.stdin(unsafe { std::process::Stdio::from_raw_fd(self.file.as_raw_fd())}); + let output = run_command(command, None)?; + let result: Result = serde_json::from_str(&output)?; + result.map_err(|err| format_err!("{}", err)) + } } diff --git a/src/tape/drive/mod.rs b/src/tape/drive/mod.rs index 7c4262c2..db1a89a3 100644 --- a/src/tape/drive/mod.rs +++ b/src/tape/drive/mod.rs @@ -4,6 +4,9 @@ mod linux_mtio; mod tape_alert_flags; pub use tape_alert_flags::*; +mod volume_statistics; +pub use volume_statistics::*; + pub mod linux_tape; mod mam; diff --git a/src/tape/drive/volume_statistics.rs b/src/tape/drive/volume_statistics.rs new file mode 100644 index 00000000..85c2acc0 --- /dev/null +++ b/src/tape/drive/volume_statistics.rs @@ -0,0 +1,266 @@ +use std::io::Read; +use std::os::unix::io::AsRawFd; + +use anyhow::{bail, format_err, Error}; +use serde::{Serialize, Deserialize}; +use endian_trait::Endian; + +use proxmox::tools::io::ReadExt; + +use crate::{ + tape::{ + 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(file: &mut F) -> Result { + + let data = sg_read_volume_statistics(file)?; + + decode_volume_statistics(&data) +} + +fn sg_read_volume_statistics(file: &mut F) -> Result, Error> { + + let buffer_size = 8192; + let mut sg_raw = SgRaw::new(file, buffer_size)?; + + // 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) | 0x17); // Volume Statistics log page + cmd.push(0); // Subpage 0 + cmd.push(0); + cmd.push(0); + cmd.push(0); + cmd.push((buffer_size >> 8) as u8); cmd.push(0); // 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, +} + +#[derive(Default, Serialize, Deserialize)] +pub struct Lp17VolumeStatistics { + volume_mounts: u64, + volume_datasets_written: u64, + volume_recovered_write_data_errors: u64, + volume_unrecovered_write_data_errors: u64, + volume_write_servo_errors: u64, + volume_unrecovered_write_servo_errors: u64, + volume_datasets_read: u64, + volume_recovered_read_errors: u64, + volume_unrecovered_read_errors: u64, + last_mount_unrecovered_write_errors: u64, + last_mount_unrecovered_read_errors: u64, + last_mount_bytes_written: u64, + last_mount_bytes_read: u64, + lifetime_bytes_written: u64, + lifetime_bytes_read: u64, + last_load_write_compression_ratio: u64, + last_load_read_compression_ratio: u64, + medium_mount_time: u64, + medium_ready_time: u64, + total_native_capacity: u64, + total_used_native_capacity: u64, + write_protect: bool, + worm: bool, + beginning_of_medium_passes: u64, + middle_of_tape_passes: u64, + serial: String, +} + +//impl Default for Lp17VolumeStatistics { + +fn decode_volume_statistics(data: &[u8]) -> Result { + + + 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 mut value: u64 = 0; + + for i in 0..len { + value = value << 8; + value = value | buffer[i] 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()? }; + + if (page_len as usize + 4) != data.len() { + bail!("invalid page length"); + } + + 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)) +}