From c3747b93c8fa450d2e5940d2db489f5187bafe44 Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Mon, 25 Jan 2021 17:44:28 +0100 Subject: [PATCH] tape: add new command line tool "pmtx" Also improve sgutil2 error reporting --- src/bin/pmtx.rs | 446 ++++++++++++++++++++++++++++++++++++++++ src/tape/changer/mod.rs | 30 ++- src/tools/sgutils2.rs | 301 +++++++++++++++++++++++---- 3 files changed, 727 insertions(+), 50 deletions(-) create mode 100644 src/bin/pmtx.rs diff --git a/src/bin/pmtx.rs b/src/bin/pmtx.rs new file mode 100644 index 00000000..a59a2a8d --- /dev/null +++ b/src/bin/pmtx.rs @@ -0,0 +1,446 @@ +/// 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 proxmox_backup::{ + tools::sgutils2::{ + scsi_inquiry, + }, + api2::types::{ + SCSI_CHANGER_PATH_SCHEMA, + CHANGER_NAME_SCHEMA, + ScsiTapeChanger, + LinuxTapeDrive, + }, + tape::{ + complete_changer_path, + changer::{ + ElementStatus, + sg_pt_changer, + }, + }, + config::{ + self, + drive::{ + complete_changer_name, + } + }, +}; + +fn get_changer_handle(param: &Value) -> Result { + + if let Some(name) = param["changer"].as_str() { + let (config, _digest) = 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) = config::drive::config()?; + let drive: LinuxTapeDrive = config.lookup("linux", &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, +) -> 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, + drivenum: Option, +) -> 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(()) +} + +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() + .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( + "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(()) +} diff --git a/src/tape/changer/mod.rs b/src/tape/changer/mod.rs index cbcec650..8f19b16f 100644 --- a/src/tape/changer/mod.rs +++ b/src/tape/changer/mod.rs @@ -11,6 +11,7 @@ mod online_status_map; pub use online_status_map::*; use anyhow::{bail, Error}; +use serde::{Serialize, Deserialize}; use crate::api2::types::{ ScsiTapeChanger, @@ -21,6 +22,7 @@ use crate::api2::types::{ /// /// Drive and slots may be `Empty`, or contain some media, either /// with knwon volume tag `VolumeTag(String)`, or without (`Full`). +#[derive(Serialize, Deserialize, Debug)] pub enum ElementStatus { Empty, Full, @@ -28,6 +30,7 @@ pub enum ElementStatus { } /// Changer drive status. +#[derive(Serialize, Deserialize)] pub struct DriveStatus { /// The slot the element was loaded from (if known). pub loaded_slot: Option, @@ -40,6 +43,7 @@ pub struct DriveStatus { } /// Storage element status. +#[derive(Serialize, Deserialize)] pub struct StorageElementStatus { /// Flag for Import/Export slots pub import_export: bool, @@ -50,6 +54,7 @@ pub struct StorageElementStatus { } /// Transport element status. +#[derive(Serialize, Deserialize)] pub struct TransportElementStatus { /// The status. pub status: ElementStatus, @@ -58,6 +63,7 @@ pub struct TransportElementStatus { } /// Changer status - show drive/slot usage +#[derive(Serialize, Deserialize)] pub struct MtxStatus { /// List of known drives pub drives: Vec, @@ -100,6 +106,20 @@ impl MtxStatus { .map(|t| t.element_address) .unwrap_or(0u16) } + + pub fn find_free_slot(&self, import_export: bool) -> Option { + 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 + } } /// Interface to SCSI changer devices @@ -333,15 +353,7 @@ pub trait MediaChange { } } - let mut free_slot = None; - for i in 0..status.slots.len() { - if status.slots[i].import_export { continue; } // skip import/export slots - if let ElementStatus::Empty = status.slots[i].status { - free_slot = Some((i+1) as u64); - break; - } - } - if let Some(slot) = free_slot { + if let Some(slot) = status.find_free_slot(false) { self.unload_media(Some(slot)) } else { bail!("drive '{}' unload failure - no free slot", self.drive_name()); diff --git a/src/tools/sgutils2.rs b/src/tools/sgutils2.rs index 94a8a7d7..32f891c0 100644 --- a/src/tools/sgutils2.rs +++ b/src/tools/sgutils2.rs @@ -9,10 +9,152 @@ 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 proxmox::tools::io::ReadExt; +#[derive(Debug)] +pub struct SenseInfo { + pub sense_key: u8, + pub asc: u8, + pub ascq: u8, +} + +impl ToString for SenseInfo { + + fn to_string(&self) -> String { + + // Added codes from IBM TS4300 Tape Library SCSI reference manual + // Added codes from Quantum Intelligent Libraries SCSI Reference Guide + match (self.sense_key, self.asc, self.ascq) { + // Not Ready + (0x02, 0x04, 0x00) => String::from("Not ready, cause not reportable"), + (0x02, 0x04, 0x01) => String::from("Not ready, operation in progress"), + (0x02, 0x04, 0x03) => String::from("Not ready, manual intervention required"), + (0x02, 0x04, 0x12) => String::from("Not ready, offline"), + (0x02, 0x04, 0x83) => String::from("The library is not ready due to aisle power being disabled"), + (0x02, 0x04, 0x8D) => String::from(" The library is not ready because it is offline"), + (0x02, 0x3B, 0x12) => String::from("Not ready, magazine removed"), + // Media Error + (0x03, 0x30, 0x00) => String::from("Media error"), + (0x03, 0x30, 0x07) => String::from("Cleaning failure"), + // Hardware Error + (0x04, 0x15, 0x01) => String::from("A mechanical positioning error occurred"), + (0x04, 0x3B, 0x0D) => String::from("Medium destination element full"), + (0x04, 0x3B, 0x0E) => String::from("Medium source element empty"), + (0x04, 0x3F, 0x0F) => String::from("Echo buffer overwritten"), + (0x04, 0x40, 0x80) => String::from("Component failure"), + (0x04, 0x44, 0x00) => String::from("Firmware detected an internal logic failure"), + (0x04, 0x53, 0x00) => String::from("A drive did not load or unload a tape"), + (0x04, 0x53, 0x01) => String::from("A drive did not unload a cartridge"), + (0x04, 0x53, 0x82) => String::from("Cannot lock the I/E station"), + (0x04, 0x53, 0x83) => String::from("Cannot unlock the I/E station"), + (0x04, 0x80, 0xD7) => String::from("Internal software error"), + (0x04, 0x80, 0xD8) => String::from("Database access error"), + (0x04, 0x81, 0xB0) => String::from("Internal system communication failed"), + (0x04, 0x81, 0xB2) => String::from("Robotic controller communication failed"), + (0x04, 0x81, 0xB3) => String::from("Mechanical positioning error"), + (0x04, 0x81, 0xB4) => String::from("Cartridge did not transport completely."), + (0x04, 0x82, 0xFC) => String::from("Drive configuration failed"), + (0x04, 0x83, 0x00) => String::from("Label too short or too long"), + // Illegal Request + (0x05, 0x04, 0x83) => String::from("Door open"), + (0x05, 0x1A, 0x00) => String::from("Parameter length error"), + (0x05, 0x20, 0x00) => String::from("Invalid command operation code"), + (0x05, 0x21, 0x01) => String::from("Invalid element address"), + (0x05, 0x24, 0x00) => String::from("Invalid field CDB"), + (0x05, 0x25, 0x00) => String::from("Invalid LUN"), + (0x05, 0x26, 0x00) => String::from("Invalid field in parameter list"), + (0x05, 0x26, 0x01) => String::from("Parameter list error: parameter not supported"), + (0x05, 0x26, 0x02) => String::from("Parameter value invalid"), + (0x05, 0x26, 0x04) => String::from("Invalid release of persistent reservation"), + (0x05, 0x2C, 0x00) => String::from("Saving parameters is not supported"), + (0x05, 0x30, 0x00) => String::from("Incompatible medium installed"), + (0x05, 0x30, 0x12) => String::from("Incompatible Media loaded to drive"), + (0x05, 0x39, 0x00) => String::from("Saving parameters is not supported"), + (0x05, 0x3B, 0x0D) => String::from("Medium destination element full"), + (0x05, 0x3B, 0x0E) => String::from("Medium source element empty"), + (0x05, 0x3B, 0x11) => String::from("Magazine not accessible"), + (0x05, 0x3B, 0x12) => String::from("Magazine not installed"), + (0x05, 0x3B, 0x18) => String::from("Element disabled"), + (0x05, 0x3B, 0x1A) => String::from("Data transfer element removed"), + (0x05, 0x3B, 0xA0) => String::from("Media type does not match destination media type"), + (0x05, 0x3F, 0x01) => String::from("New firmware loaded"), + (0x05, 0x3F, 0x03) => String::from("Inquiry data changed"), + (0x05, 0x44, 0x81) => String::from("Source element not ready"), + (0x05, 0x44, 0x82) => String::from("Destination element not ready"), + (0x05, 0x53, 0x02) => String::from("Library media removal prevented state set."), + (0x05, 0x53, 0x03) => String::from("Drive media removal prevented state set"), + (0x05, 0x53, 0x81) => String::from("Insert/Eject station door is open"), + (0x05, 0x82, 0x93) => String::from("Failure session sequence error"), + (0x05, 0x82, 0x94) => String::from("Failover command sequence error"), + (0x05, 0x82, 0x95) => String::from("Duplicate failover session key"), + (0x05, 0x82, 0x96) => String::from("Invalid failover key"), + (0x05, 0x82, 0x97) => String::from("Failover session that is released"), + (0x05, 0x83, 0x02) => String::from("Barcode label questionable"), + (0x05, 0x83, 0x03) => String::from("Cell status and barcode label questionable"), + (0x05, 0x83, 0x04) => String::from("Data transfer element not installed"), + (0x05, 0x83, 0x05) => String::from("Data transfer element is varied off and not accessible for library operations"), + (0x05, 0x83, 0x06) => String::from("Element is contained within an offline tower or I/E station and is not accessible for library operations"), + // Unit Attention + (0x06, 0x28, 0x00) => String::from("Not ready to change, medium changed"), + (0x06, 0x28, 0x01) => String::from("Import/export element that is accessed"), + (0x06, 0x29, 0x00) => String::from("Power-on or reset occurred"), + (0x06, 0x29, 0x01) => String::from("Power on occurred"), + (0x06, 0x29, 0x02) => String::from("SCSI Bus reset occurred"), + (0x06, 0x29, 0x03) => String::from("Device reset occurred"), + (0x06, 0x29, 0x04) => String::from("Internal reset occurred"), + (0x06, 0x2A, 0x01) => String::from("Mode parameters have been changed"), + (0x06, 0x2A, 0x03) => String::from("Reservations preempted"), + (0x06, 0x2A, 0x04) => String::from("Reservations released"), + (0x06, 0x2A, 0x05) => String::from("Registrations preempted"), + // Aborted Command + (0x0B, 0x3F, 0x0F) => String::from("ECHO buffer overwritten"), + (0x0B, 0x44, 0x00) => String::from("Firmware detected an internal logic failure"), + (0x0B, 0x45, 0x00) => String::from("Select or reselect failure"), + (0x0B, 0x47, 0x00) => String::from("SCSI parity error"), + (0x0B, 0x48, 0x00) => String::from("Initiator detected error message received"), + (0x0B, 0x49, 0x00) => String::from("Invalid message error"), + (0x0B, 0x4A, 0x00) => String::from("Command phase error"), + (0x0B, 0x4B, 0x00) => String::from("Data phase error"), + (0x0B, 0x4E, 0x00) => String::from("Overlapped command attempt"), + // Else, simply report values + _ => format!("sense_key = 0x{:02x}, ASC = 0x{:02x}, ASCQ = 0x{:02x}", self.sense_key, self.asc, self.ascq), + } + } +} + +#[derive(Debug)] +pub struct ScsiError { + pub error: Error, + pub sense: Option, +} + +impl std::fmt::Display for ScsiError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.error) + } +} + +impl std::error::Error for ScsiError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.error.source() + } +} + +impl From for ScsiError { + fn from(error: anyhow::Error) -> Self { + Self { error, sense: None } + } +} + +impl From for ScsiError { + fn from(error: std::io::Error) -> Self { + Self { error: error.into(), sense: None } + } +} + // Opaque wrapper for sg_pt_base #[repr(C)] struct SgPtBase { _private: [u8; 0] } @@ -83,6 +225,22 @@ pub const PERIPHERAL_DEVICE_TYPE_TEXT: [&'static str; 32] = [ "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; + + #[repr(C, packed)] #[derive(Endian)] struct InquiryPage { @@ -100,8 +258,23 @@ struct InquiryPage { // additional data follows, but we do not need that } +#[repr(C, packed)] +#[derive(Endian, Debug)] +struct RequestSensePage { + response_code: u8, + obsolete: u8, + flags2: u8, + information: [u8;4], + additional_sense_len: u8, + command_specific_information: [u8;4], + additional_sense_code: u8, + additional_sense_code_qualifier: u8, + field_replacable_unit_code: u8, + sense_key_specific: [u8; 3], +} + /// Inquiry result -#[derive(Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct InquiryInfo { /// Peripheral device type (0-31) pub peripheral_type: u8, @@ -115,6 +288,10 @@ pub struct InquiryInfo { pub revision: String, } +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; @@ -176,8 +353,9 @@ extern "C" { fn get_scsi_pt_status_response(objp: *const SgPtBase) -> c_int; - #[allow(dead_code)] fn get_scsi_pt_result_category(objp: *const SgPtBase) -> c_int; + + fn get_scsi_pt_os_err(objp: *const SgPtBase) -> c_int; } /// Safe interface to run RAW SCSI commands @@ -258,15 +436,88 @@ impl <'a, F: AsRawFd> SgRaw<'a, F> { 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 => { /* Ok */ } + SCSI_PT_RESULT_STATUS => { /* test below */ } + SCSI_PT_RESULT_SENSE => { + if sense_len == 0 { + return Err(format_err!("scsi command failed: no Sense").into()); + } + let mut reader = &self.sense_buffer[..(sense_len as usize)]; + + let sense: RequestSensePage = unsafe { reader.read_be_value()? }; + + let code = sense.response_code & 0x7f; + if code == 0x71 { + return Err(format_err!("scsi command failed: received deferred Sense").into()); + } + if code != 0x70 { + return Err(format_err!("scsi command failed: invalid Sense response code {:x}", code).into()); + } + + let sense = SenseInfo { + sense_key: sense.flags2 & 0xf, + asc: sense.additional_sense_code, + ascq: sense.additional_sense_code_qualifier, + }; + + return Err(ScsiError { + error: format_err!("scsi command failed: {}", sense.to_string()), + sense: Some(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()), + } + + 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()); + } + + Ok(()) + } + /// Run the specified RAW SCSI command - pub fn do_command(&mut self, cmd: &[u8]) -> Result<&[u8], Error> { + 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) } { - bail!("no valid SCSI command"); + return Err(format_err!("no valid SCSI command").into()); } if self.buffer.len() < 16 { - bail!("output buffer too small"); + return Err(format_err!("output buffer too small").into()); } let mut ptvp = self.create_scsi_pt_obj()?; @@ -279,27 +530,11 @@ impl <'a, F: AsRawFd> SgRaw<'a, F> { ) }; - let res = unsafe { do_scsi_pt(ptvp.as_mut_ptr(), self.file.as_raw_fd(), self.timeout, 0) }; - if res < 0 { - let err = nix::Error::last(); - bail!("do_scsi_pt failed - {}", err); - } - if res != 0 { - bail!("do_scsi_pt failed {}", res); - } - - // todo: what about sense data? - let _sense_len = unsafe { get_scsi_pt_sense_len(ptvp.as_ptr()) }; - - let status = unsafe { get_scsi_pt_status_response(ptvp.as_ptr()) }; - if status != 0 { - // toto: improve error reporting - bail!("unknown scsi error - status response {}", status); - } + self.do_scsi_pt_checked(&mut ptvp)?; let resid = unsafe { get_scsi_pt_resid(ptvp.as_ptr()) } as usize; if resid > self.buffer.len() { - bail!("do_scsi_pt failed - got strange resid (value too big)"); + return Err(format_err!("do_scsi_pt failed - got strange resid (value too big)").into()); } let data_len = self.buffer.len() - resid; @@ -334,25 +569,9 @@ impl <'a, F: AsRawFd> SgRaw<'a, F> { cmd.as_ptr(), cmd.len() as c_int, ); - }; + }; - let res = unsafe { do_scsi_pt(ptvp.as_mut_ptr(), self.file.as_raw_fd(), self.timeout, 0) }; - if res < 0 { - let err = nix::Error::last(); - bail!("do_scsi_pt failed - {}", err); - } - if res != 0 { - bail!("do_scsi_pt failed {}", res); - } - - // todo: what about sense data? - let _sense_len = unsafe { get_scsi_pt_sense_len(ptvp.as_ptr()) }; - - let status = unsafe { get_scsi_pt_status_response(ptvp.as_ptr()) }; - if status != 0 { - // toto: improve error reporting - bail!("unknown scsi error - status response {}", status); - } + self.do_scsi_pt_checked(&mut ptvp)?; Ok(()) }