diff --git a/debian/control.in b/debian/control.in index 1633f209..cd9c2bdd 100644 --- a/debian/control.in +++ b/debian/control.in @@ -14,6 +14,7 @@ Depends: fonts-font-awesome, smartmontools, mtx, mt-st, + sg3-utils, ${misc:Depends}, ${shlibs:Depends}, Recommends: zfsutils-linux, diff --git a/src/api2/tape/drive.rs b/src/api2/tape/drive.rs index 05afea31..5932ef4d 100644 --- a/src/api2/tape/drive.rs +++ b/src/api2/tape/drive.rs @@ -34,6 +34,7 @@ use crate::{ TapeDeviceInfo, MediaIdFlat, LabelUuidMap, + MamAttribute, }, server::WorkerTask, tape::{ @@ -46,6 +47,7 @@ use crate::{ mtx_load, mtx_unload, linux_tape_device_list, + read_mam_attributes, open_drive, media_changer, update_changer_online_status, @@ -768,6 +770,32 @@ fn barcode_label_media_worker( Ok(()) } +#[api( + input: { + properties: { + drive: { + schema: DRIVE_NAME_SCHEMA, + }, + }, + }, + returns: { + description: "A List of medium auxiliary memory attributes.", + type: Array, + items: { + type: MamAttribute, + }, + }, +)] +/// Read Medium auxiliary memory attributes (Cartridge Memory) +pub fn mam_attributes(drive: String) -> Result, Error> { + + let (config, _digest) = config::drive::config()?; + + let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?; + + read_mam_attributes(&drive_config.path) +} + #[sortable] pub const SUBDIRS: SubdirMap = &sorted!([ ( @@ -801,6 +829,11 @@ pub const SUBDIRS: SubdirMap = &sorted!([ &Router::new() .put(&API_METHOD_LOAD_SLOT) ), + ( + "mam-attributes", + &Router::new() + .put(&API_METHOD_MAM_ATTRIBUTES) + ), ( "read-label", &Router::new() diff --git a/src/api2/types/tape/drive.rs b/src/api2/types/tape/drive.rs index b3a68ba1..aff3373e 100644 --- a/src/api2/types/tape/drive.rs +++ b/src/api2/types/tape/drive.rs @@ -99,3 +99,15 @@ pub struct DriveListEntry { #[serde(skip_serializing_if="Option::is_none")] pub serial: Option, } + +#[api()] +#[derive(Serialize,Deserialize)] +/// Medium auxiliary memory attributes (MAM) +pub struct MamAttribute { + /// Attribute id + pub id: u16, + /// Attribute name + pub name: String, + /// Attribute value + pub value: String, +} diff --git a/src/bin/proxmox-tape.rs b/src/bin/proxmox-tape.rs index a6cb8852..70d20ddf 100644 --- a/src/bin/proxmox-tape.rs +++ b/src/bin/proxmox-tape.rs @@ -512,6 +512,48 @@ fn debug_scan(param: Value) -> Result<(), Error> { } } +#[api( + input: { + properties: { + drive: { + schema: DRIVE_NAME_SCHEMA, + optional: true, + }, + "output-format": { + schema: OUTPUT_FORMAT, + optional: true, + }, + }, + }, +)] +/// Read Medium auxiliary memory attributes (Cartridge Memory) +fn mam_attributes( + mut param: Value, + rpcenv: &mut dyn RpcEnvironment, +) -> Result<(), Error> { + + let (config, _digest) = config::drive::config()?; + + param["drive"] = lookup_drive_name(¶m, &config)?.into(); + + let output_format = get_output_format(¶m); + let info = &api2::tape::drive::API_METHOD_MAM_ATTRIBUTES; + + let mut data = match info.handler { + ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?, + _ => unreachable!(), + }; + + let options = default_table_format_options() + .column(ColumnConfig::new("id")) + .column(ColumnConfig::new("name")) + .column(ColumnConfig::new("value")) + ; + + format_and_print_result_full(&mut data, info.returns, &output_format, &options); + Ok(()) +} + #[api( input: { properties: { @@ -593,6 +635,11 @@ fn main() { CliCommand::new(&API_METHOD_READ_LABEL) .completion_cb("drive", complete_drive_name) ) + .insert( + "mam", + CliCommand::new(&API_METHOD_MAM_ATTRIBUTES) + .completion_cb("drive", complete_drive_name) + ) .insert( "label", CliCommand::new(&API_METHOD_LABEL_MEDIA) diff --git a/src/tape/drive/mam.rs b/src/tape/drive/mam.rs new file mode 100644 index 00000000..e22da38b --- /dev/null +++ b/src/tape/drive/mam.rs @@ -0,0 +1,175 @@ +use std::collections::HashMap; +use std::convert::TryInto; + +use anyhow::{bail, format_err, Error}; +use endian_trait::Endian; + +use proxmox::tools::io::ReadExt; + +use crate::api2::types::MamAttribute; + +// 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: &'static [ (u16, u16, MamFormat, &'static str) ] = &[ + (0x00_00, 8, MamFormat::DEC, "Remaining Capacity In Partition"), + (0x00_01, 8, MamFormat::DEC, "Maximum Capacity In Partition"), + (0x00_02, 8, MamFormat::BINARY, "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 Identifer"), + (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 Identifer"), + (0x08_21, 36, MamFormat::ASCII, "Media Pool Globally Unique Identifer"), + + (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 = { + let mut map = HashMap::new(); + + for entry in MAM_ATTRIBUTES { + map.insert(entry.0, entry); + } + + map + }; +} + +fn read_tape_mam(path: &str) -> Result, Error> { + + let mut command = std::process::Command::new("sg_raw"); + command.args(&[ + "-r", "32k", + "-o", "-", + path, + "8c", "00", "00", "00", "00", "00", "00", "00", + "00", "00", // first attribute + "00", "00", "8f", "ff", // alloc len + "00", "00", + ]); + + let output = command.output() + .map_err(|err| format_err!("failed to execute {:?} - {}", command, err))?; + + Ok(output.stdout) +} + +pub fn read_mam_attributes(path: &str) -> Result, Error> { + + let data = read_tape_mam(path)?; + + 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); + } + + 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 { + 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) +} diff --git a/src/tape/drive/mod.rs b/src/tape/drive/mod.rs index 67a48a05..883ce51d 100644 --- a/src/tape/drive/mod.rs +++ b/src/tape/drive/mod.rs @@ -2,6 +2,9 @@ mod virtual_tape; mod linux_mtio; mod linux_tape; +mod mam; +pub use mam::*; + mod linux_list_drives; pub use linux_list_drives::*;