2021-01-25 09:15:59 +00:00
|
|
|
//! SCSI changer implementation using libsgutil2
|
|
|
|
use std::collections::HashMap;
|
2022-04-06 15:00:29 +00:00
|
|
|
use std::fs::{File, OpenOptions};
|
|
|
|
use std::io::Read;
|
|
|
|
use std::os::unix::prelude::AsRawFd;
|
2021-01-25 09:15:59 +00:00
|
|
|
use std::path::Path;
|
|
|
|
|
|
|
|
use anyhow::{bail, format_err, Error};
|
|
|
|
use endian_trait::Endian;
|
|
|
|
|
2021-10-08 09:19:37 +00:00
|
|
|
use proxmox_io::ReadExt;
|
2021-01-25 09:15:59 +00:00
|
|
|
|
2021-09-10 10:25:32 +00:00
|
|
|
use pbs_api_types::ScsiTapeChanger;
|
|
|
|
|
2021-01-25 09:15:59 +00:00
|
|
|
use crate::{
|
2022-04-06 15:00:29 +00:00
|
|
|
sgutils2::{scsi_ascii_to_string, scsi_inquiry, ScsiError, SgRaw, SENSE_KEY_NOT_READY},
|
|
|
|
DriveStatus, ElementStatus, MtxStatus, StorageElementStatus, TransportElementStatus,
|
2021-01-25 09:15:59 +00:00
|
|
|
};
|
|
|
|
|
2022-04-06 15:00:29 +00:00
|
|
|
const SCSI_CHANGER_DEFAULT_TIMEOUT: usize = 60 * 5; // 5 minutes
|
2021-07-21 14:04:49 +00:00
|
|
|
const SCSI_VOLUME_TAG_LEN: usize = 36;
|
2021-01-25 09:15:59 +00:00
|
|
|
|
|
|
|
/// 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)
|
2022-04-06 15:00:29 +00:00
|
|
|
sg_raw.set_timeout(30 * 60);
|
2021-01-25 09:15:59 +00:00
|
|
|
|
|
|
|
let mut cmd = Vec::new();
|
|
|
|
cmd.extend(&[0x07, 0, 0, 0, 0, 0]); // INITIALIZE ELEMENT STATUS (07h)
|
|
|
|
|
2022-04-06 15:00:29 +00:00
|
|
|
sg_raw
|
|
|
|
.do_command(&cmd)
|
2021-01-25 09:15:59 +00:00
|
|
|
.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,
|
|
|
|
}
|
|
|
|
|
2021-01-27 08:34:24 +00:00
|
|
|
/// Execute scsi commands, optionally repeat the command until
|
2021-07-13 08:39:28 +00:00
|
|
|
/// successful or timeout (sleep 1 second between invovations)
|
2021-01-27 07:59:10 +00:00
|
|
|
///
|
2021-07-13 08:39:28 +00:00
|
|
|
/// Timeout is 5 seconds. If the device reports "Not Ready - becoming
|
|
|
|
/// ready", we wait up to 5 minutes.
|
2021-01-27 07:59:10 +00:00
|
|
|
///
|
|
|
|
/// Skipped errors are printed on stderr.
|
2021-01-27 08:34:24 +00:00
|
|
|
fn execute_scsi_command<F: AsRawFd>(
|
2021-01-27 07:59:10 +00:00
|
|
|
sg_raw: &mut SgRaw<F>,
|
|
|
|
cmd: &[u8],
|
|
|
|
error_prefix: &str,
|
2021-01-27 08:34:24 +00:00
|
|
|
retry: bool,
|
2021-01-27 07:59:10 +00:00
|
|
|
) -> 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 {
|
2021-12-30 11:57:37 +00:00
|
|
|
match sg_raw.do_command(cmd) {
|
2021-01-27 07:59:10 +00:00
|
|
|
Ok(data) => return Ok(data.to_vec()),
|
2021-07-13 08:39:28 +00:00
|
|
|
Err(err) if !retry => bail!("{} failed: {}", error_prefix, err),
|
2021-01-27 07:59:10 +00:00
|
|
|
Err(err) => {
|
2021-07-13 08:39:28 +00:00
|
|
|
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);
|
2021-01-27 08:34:24 +00:00
|
|
|
}
|
2021-01-27 07:59:10 +00:00
|
|
|
|
2021-07-13 08:39:28 +00:00
|
|
|
if let ScsiError::Sense(ref sense) = err {
|
|
|
|
// Not Ready - becoming ready
|
2022-04-06 15:00:29 +00:00
|
|
|
if sense.sense_key == SENSE_KEY_NOT_READY
|
|
|
|
&& sense.asc == 0x04
|
|
|
|
&& sense.ascq == 1
|
|
|
|
{
|
2021-07-13 08:39:28 +00:00
|
|
|
// wait up to 5 minutes, long enough to finish inventorize
|
2022-04-06 15:00:29 +00:00
|
|
|
timeout = std::time::Duration::new(5 * 60, 0);
|
2021-01-27 07:59:10 +00:00
|
|
|
}
|
|
|
|
}
|
2021-07-13 08:39:28 +00:00
|
|
|
|
|
|
|
if start.elapsed()? > timeout {
|
|
|
|
bail!("{} failed: {}", error_prefix, err);
|
|
|
|
}
|
|
|
|
|
|
|
|
std::thread::sleep(std::time::Duration::new(1, 0));
|
|
|
|
continue; // try again
|
2021-01-27 07:59:10 +00:00
|
|
|
}
|
|
|
|
}
|
2022-04-06 15:00:29 +00:00
|
|
|
}
|
2021-01-27 07:59:10 +00:00
|
|
|
}
|
|
|
|
|
2021-01-25 09:15:59 +00:00
|
|
|
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
|
|
|
|
|
2021-01-27 08:34:24 +00:00
|
|
|
let data = execute_scsi_command(&mut sg_raw, &cmd, "read element address assignment", true)?;
|
2021-01-25 09:15:59 +00:00
|
|
|
|
2021-10-08 09:19:37 +00:00
|
|
|
proxmox_lang::try_block!({
|
2021-01-25 09:15:59 +00:00
|
|
|
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)
|
2022-04-06 15:00:29 +00:00
|
|
|
})
|
|
|
|
.map_err(|err: Error| format_err!("decode element address assignment page failed - {}", err))
|
2021-01-25 09:15:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2022-04-06 15:00:29 +00:00
|
|
|
pub fn load_slot(file: &mut File, from_slot: u64, drivenum: u64) -> Result<(), Error> {
|
2021-01-25 09:15:59 +00:00
|
|
|
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);
|
|
|
|
|
2022-04-06 15:00:29 +00:00
|
|
|
sg_raw
|
|
|
|
.do_command(&cmd)
|
2021-01-25 09:15:59 +00:00
|
|
|
.map_err(|err| format_err!("load drive failed - {}", err))?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Unload media from drive into a storage slot
|
2022-04-06 15:00:29 +00:00
|
|
|
pub fn unload(file: &mut File, to_slot: u64, drivenum: u64) -> Result<(), Error> {
|
2021-01-25 09:15:59 +00:00
|
|
|
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);
|
|
|
|
|
2022-04-06 15:00:29 +00:00
|
|
|
sg_raw
|
|
|
|
.do_command(&cmd)
|
2021-01-25 09:15:59 +00:00
|
|
|
.map_err(|err| format_err!("unload drive failed - {}", err))?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-03-10 15:37:09 +00:00
|
|
|
/// Transfer medium from one storage slot to another
|
2021-01-25 09:15:59 +00:00
|
|
|
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);
|
|
|
|
|
2022-04-06 15:00:29 +00:00
|
|
|
sg_raw.do_command(&cmd).map_err(|err| {
|
|
|
|
format_err!(
|
|
|
|
"transfer medium from slot {} to slot {} failed - {}",
|
|
|
|
from_slot,
|
|
|
|
to_slot,
|
|
|
|
err
|
|
|
|
)
|
|
|
|
})?;
|
2021-01-25 09:15:59 +00:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-07-12 15:48:53 +00:00
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
enum ElementType {
|
2021-07-15 11:07:20 +00:00
|
|
|
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
|
2022-04-06 15:00:29 +00:00
|
|
|
_ => 0b000, // Mixed=0,CurData=0,DVCID=0
|
2021-07-15 11:07:20 +00:00
|
|
|
}
|
|
|
|
}
|
2021-07-12 15:48:53 +00:00
|
|
|
}
|
|
|
|
|
2021-01-25 09:15:59 +00:00
|
|
|
fn scsi_read_element_status_cdb(
|
|
|
|
start_element_address: u16,
|
2021-07-12 15:48:53 +00:00
|
|
|
number_of_elements: u16,
|
|
|
|
element_type: ElementType,
|
2021-01-25 09:15:59 +00:00
|
|
|
allocation_len: u32,
|
|
|
|
) -> Vec<u8> {
|
|
|
|
let mut cmd = Vec::new();
|
|
|
|
cmd.push(0xB8); // READ ELEMENT STATUS (B8h)
|
2021-07-15 11:07:20 +00:00
|
|
|
cmd.push(element_type.byte1());
|
2021-01-25 09:15:59 +00:00
|
|
|
cmd.extend(&start_element_address.to_be_bytes());
|
|
|
|
|
|
|
|
cmd.extend(&number_of_elements.to_be_bytes());
|
2021-07-15 11:07:20 +00:00
|
|
|
cmd.push(element_type.byte6());
|
2021-01-25 09:15:59 +00:00
|
|
|
cmd.extend(&allocation_len.to_be_bytes()[1..4]);
|
|
|
|
cmd.push(0);
|
|
|
|
cmd.push(0);
|
|
|
|
|
|
|
|
cmd
|
|
|
|
}
|
|
|
|
|
2021-07-12 15:48:53 +00:00
|
|
|
// 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 {
|
2022-04-06 15:00:29 +00:00
|
|
|
let cmd = scsi_read_element_status_cdb(
|
|
|
|
start_element_address,
|
|
|
|
number_of_elements,
|
|
|
|
element_type,
|
|
|
|
allocation_len,
|
|
|
|
);
|
2021-07-12 15:48:53 +00:00
|
|
|
|
|
|
|
let data = execute_scsi_command(sg_raw, &cmd, "read element status (B8h)", retry)?;
|
|
|
|
|
2021-07-28 10:05:09 +00:00
|
|
|
let page = decode_element_status_page(&data, start_element_address)?;
|
2021-07-12 15:48:53 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2021-01-25 09:15:59 +00:00
|
|
|
/// 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();
|
|
|
|
|
2021-07-28 10:05:09 +00:00
|
|
|
let page = get_element(&mut sg_raw, ElementType::Storage, allocation_len, true)?;
|
2021-07-12 15:48:53 +00:00
|
|
|
storage_slots.extend(page.storage_slots);
|
2021-01-27 08:34:24 +00:00
|
|
|
|
2022-04-06 15:00:29 +00:00
|
|
|
let page = get_element(
|
|
|
|
&mut sg_raw,
|
|
|
|
ElementType::ImportExport,
|
|
|
|
allocation_len,
|
|
|
|
false,
|
|
|
|
)?;
|
2021-07-12 15:48:53 +00:00
|
|
|
import_export_slots.extend(page.import_export_slots);
|
2021-01-25 09:15:59 +00:00
|
|
|
|
2022-04-06 15:00:29 +00:00
|
|
|
let page = get_element(
|
|
|
|
&mut sg_raw,
|
|
|
|
ElementType::DataTransfer,
|
|
|
|
allocation_len,
|
|
|
|
false,
|
|
|
|
)?;
|
2021-07-12 15:48:53 +00:00
|
|
|
drives.extend(page.drives);
|
2021-01-25 09:15:59 +00:00
|
|
|
|
2021-07-15 11:07:20 +00:00
|
|
|
// get the serial + vendor + model,
|
|
|
|
// some changer require this to be an extra scsi command
|
2022-04-06 15:00:29 +00:00
|
|
|
let page = get_element(
|
|
|
|
&mut sg_raw,
|
|
|
|
ElementType::DataTransferWithDVCID,
|
|
|
|
allocation_len,
|
|
|
|
false,
|
|
|
|
)?;
|
2021-07-15 11:07:20 +00:00
|
|
|
// 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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-06 15:00:29 +00:00
|
|
|
let page = get_element(
|
|
|
|
&mut sg_raw,
|
|
|
|
ElementType::MediumTransport,
|
|
|
|
allocation_len,
|
|
|
|
false,
|
|
|
|
)?;
|
2021-07-12 15:48:53 +00:00
|
|
|
transports.extend(page.transports);
|
2021-01-25 09:15:59 +00:00
|
|
|
|
2021-07-22 09:26:30 +00:00
|
|
|
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()
|
|
|
|
);
|
2021-01-25 09:15:59 +00:00
|
|
|
}
|
2021-07-22 09:26:30 +00:00
|
|
|
if storage_count != storage_slots.len() {
|
|
|
|
bail!(
|
|
|
|
"got wrong number of storage elements: expected {}, got {}",
|
|
|
|
storage_count,
|
|
|
|
storage_slots.len(),
|
|
|
|
);
|
2021-01-25 09:15:59 +00:00
|
|
|
}
|
2021-07-22 09:26:30 +00:00
|
|
|
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(),
|
|
|
|
);
|
2021-01-25 09:15:59 +00:00
|
|
|
}
|
2021-07-22 09:26:30 +00:00
|
|
|
if transfer_count != drives.len() {
|
|
|
|
bail!(
|
|
|
|
"got wrong number of transfer elements: expected {}, got {}",
|
|
|
|
transfer_count,
|
|
|
|
drives.len(),
|
|
|
|
);
|
2021-01-25 09:15:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
2022-04-06 15:00:29 +00:00
|
|
|
let mut status = MtxStatus {
|
|
|
|
transports,
|
|
|
|
drives,
|
|
|
|
slots,
|
|
|
|
};
|
2021-01-25 09:15:59 +00:00
|
|
|
|
|
|
|
// sanity checks
|
|
|
|
if status.drives.is_empty() {
|
|
|
|
bail!("no data transfer elements reported");
|
|
|
|
}
|
|
|
|
if status.slots.is_empty() {
|
2022-04-06 15:00:29 +00:00
|
|
|
bail!("no storage elements reported");
|
2021-01-25 09:15:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
2022-02-08 13:57:16 +00:00
|
|
|
drive.loaded_slot = slot_map.get(&source_address).copied();
|
2021-01-25 09:15:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(status)
|
|
|
|
}
|
|
|
|
|
2021-01-28 11:59:54 +00:00
|
|
|
/// Read status and map import-export slots from config
|
|
|
|
pub fn status(config: &ScsiTapeChanger) -> Result<MtxStatus, Error> {
|
|
|
|
let path = &config.path;
|
|
|
|
|
2022-04-06 15:00:29 +00:00
|
|
|
let mut file = open(path).map_err(|err| format_err!("error opening '{}': {}", path, err))?;
|
2021-01-28 11:59:54 +00:00
|
|
|
let mut status = read_element_status(&mut file)
|
|
|
|
.map_err(|err| format_err!("error reading element status: {}", err))?;
|
|
|
|
|
2021-12-30 11:57:37 +00:00
|
|
|
status.mark_import_export_slots(config)?;
|
2021-01-28 11:59:54 +00:00
|
|
|
|
|
|
|
Ok(status)
|
|
|
|
}
|
|
|
|
|
2021-01-25 09:15:59 +00:00
|
|
|
#[repr(C, packed)]
|
|
|
|
#[derive(Endian)]
|
|
|
|
struct ElementStatusHeader {
|
|
|
|
first_element_address_reported: u16,
|
|
|
|
number_of_elements_available: u16,
|
|
|
|
reserved: u8,
|
2022-04-06 15:00:29 +00:00
|
|
|
byte_count_of_report_available: [u8; 3],
|
2021-01-25 09:15:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[repr(C, packed)]
|
|
|
|
#[derive(Endian)]
|
|
|
|
struct SubHeader {
|
|
|
|
element_type_code: u8,
|
|
|
|
flags: u8,
|
|
|
|
descriptor_length: u16,
|
2021-03-10 15:37:09 +00:00
|
|
|
reserved: u8,
|
2022-04-06 15:00:29 +00:00
|
|
|
byte_count_of_descriptor_data_available: [u8; 3],
|
2021-01-25 09:15:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl SubHeader {
|
|
|
|
fn parse_optional_volume_tag<R: Read>(
|
|
|
|
&self,
|
|
|
|
reader: &mut R,
|
|
|
|
full: bool,
|
|
|
|
) -> Result<Option<String>, Error> {
|
2022-04-06 15:00:29 +00:00
|
|
|
if (self.flags & 128) != 0 {
|
|
|
|
// has PVolTag
|
2021-07-21 14:04:49 +00:00
|
|
|
let tmp = reader.read_exact_allocated(SCSI_VOLUME_TAG_LEN)?;
|
2021-01-25 09:15:59 +00:00
|
|
|
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
|
2022-04-06 15:00:29 +00:00
|
|
|
fn skip_alternate_volume_tag<R: Read>(&self, reader: &mut R) -> Result<Option<String>, Error> {
|
|
|
|
if (self.flags & 64) != 0 {
|
|
|
|
// has AVolTag
|
2021-07-21 14:04:49 +00:00
|
|
|
let _tmp = reader.read_exact_allocated(SCSI_VOLUME_TAG_LEN)?;
|
2021-01-25 09:15:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[repr(C, packed)]
|
|
|
|
#[derive(Endian)]
|
2022-04-06 15:00:29 +00:00
|
|
|
struct TransportDescriptor {
|
|
|
|
// Robot/Griper
|
2021-01-25 09:15:59 +00:00
|
|
|
element_address: u16,
|
|
|
|
flags1: u8,
|
|
|
|
reserved_3: u8,
|
|
|
|
additional_sense_code: u8,
|
|
|
|
additional_sense_code_qualifier: u8,
|
2022-04-06 15:00:29 +00:00
|
|
|
reserved_6: [u8; 3],
|
2021-01-25 09:15:59 +00:00
|
|
|
flags2: u8,
|
|
|
|
source_storage_element_address: u16,
|
|
|
|
// volume tag and Mixed media descriptor follows (depends on flags)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[repr(C, packed)]
|
|
|
|
#[derive(Endian)]
|
2022-04-06 15:00:29 +00:00
|
|
|
struct TransferDescriptor {
|
|
|
|
// Tape drive
|
2021-01-25 09:15:59 +00:00
|
|
|
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)]
|
2022-04-06 15:00:29 +00:00
|
|
|
struct DvcidHead {
|
|
|
|
// Drive Identifier Header
|
2021-01-25 09:15:59 +00:00
|
|
|
code_set: u8,
|
|
|
|
identifier_type: u8,
|
|
|
|
reserved: u8,
|
|
|
|
identifier_len: u8,
|
|
|
|
// Identifier follows
|
|
|
|
}
|
|
|
|
|
|
|
|
#[repr(C, packed)]
|
|
|
|
#[derive(Endian)]
|
2022-04-06 15:00:29 +00:00
|
|
|
struct StorageDescriptor {
|
|
|
|
// Mail Slot
|
2021-01-25 09:15:59 +00:00
|
|
|
element_address: u16,
|
|
|
|
flags1: u8,
|
|
|
|
reserved_3: u8,
|
|
|
|
additional_sense_code: u8,
|
|
|
|
additional_sense_code_qualifier: u8,
|
2022-04-06 15:00:29 +00:00
|
|
|
reserved_6: [u8; 3],
|
2021-01-25 09:15:59 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-22 09:57:02 +00:00
|
|
|
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) {
|
2022-04-06 15:00:29 +00:00
|
|
|
(2, 0) => {
|
|
|
|
// Serial number only (Quantum Superloader3 uses this)
|
2021-07-22 09:57:02 +00:00
|
|
|
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,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-01-25 09:15:59 +00:00
|
|
|
fn decode_element_status_page(
|
|
|
|
data: &[u8],
|
|
|
|
start_element_address: u16,
|
|
|
|
) -> Result<DecodedStatusPage, Error> {
|
2021-10-08 09:19:37 +00:00
|
|
|
proxmox_lang::try_block!({
|
2021-01-25 09:15:59 +00:00
|
|
|
let mut result = DecodedStatusPage {
|
|
|
|
last_element_address: None,
|
|
|
|
transports: Vec::new(),
|
|
|
|
drives: Vec::new(),
|
|
|
|
storage_slots: Vec::new(),
|
|
|
|
import_export_slots: Vec::new(),
|
|
|
|
};
|
|
|
|
|
2022-02-08 13:57:16 +00:00
|
|
|
let mut reader = data;
|
2021-01-25 09:15:59 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-07-28 10:05:10 +00:00
|
|
|
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() {
|
2022-04-06 15:00:29 +00:00
|
|
|
bail!(
|
|
|
|
"wrong amount of data: expected {}, got {}",
|
|
|
|
len,
|
|
|
|
reader.len()
|
|
|
|
);
|
2021-07-28 10:05:10 +00:00
|
|
|
}
|
|
|
|
|
2021-01-25 09:15:59 +00:00
|
|
|
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)?;
|
|
|
|
|
2021-07-26 07:20:41 +00:00
|
|
|
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) {
|
2022-02-08 13:57:16 +00:00
|
|
|
let mut reader = descriptor;
|
2021-04-21 10:24:57 +00:00
|
|
|
|
2021-01-25 09:15:59 +00:00
|
|
|
match subhead.element_type_code {
|
|
|
|
1 => {
|
2021-07-21 14:04:50 +00:00
|
|
|
let desc: TransportDescriptor = unsafe { reader.read_be_value()? };
|
2021-01-25 09:15:59 +00:00
|
|
|
|
|
|
|
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()? };
|
|
|
|
|
2022-04-06 15:00:29 +00:00
|
|
|
let loaded_slot = if (desc.flags2 & 128) != 0 {
|
|
|
|
// SValid
|
2021-01-25 09:15:59 +00:00
|
|
|
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)?;
|
|
|
|
|
2021-07-22 09:57:02 +00:00
|
|
|
let dvcid = decode_dvcid_info(&mut reader).unwrap_or(DvcidInfo {
|
|
|
|
vendor: None,
|
|
|
|
model: None,
|
|
|
|
serial: None,
|
|
|
|
});
|
2021-01-25 09:15:59 +00:00
|
|
|
|
|
|
|
result.last_element_address = Some(desc.element_address);
|
|
|
|
|
|
|
|
let drive = DriveStatus {
|
|
|
|
loaded_slot,
|
|
|
|
status: create_element_status(full, volume_tag),
|
2021-07-22 09:57:02 +00:00
|
|
|
drive_serial_number: dvcid.serial,
|
|
|
|
vendor: dvcid.vendor,
|
|
|
|
model: dvcid.model,
|
2021-01-25 09:15:59 +00:00
|
|
|
element_address: desc.element_address,
|
|
|
|
};
|
|
|
|
result.drives.push(drive);
|
|
|
|
}
|
|
|
|
code => bail!("got unknown element type code {}", code),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(result)
|
2022-04-06 15:00:29 +00:00
|
|
|
})
|
|
|
|
.map_err(|err: Error| format_err!("decode element status failed - {}", err))
|
2021-01-25 09:15:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Open the device for read/write, returns the file handle
|
|
|
|
pub fn open<P: AsRef<Path>>(path: P) -> Result<File, Error> {
|
2022-04-06 15:00:29 +00:00
|
|
|
let file = OpenOptions::new().read(true).write(true).open(path)?;
|
2021-01-25 09:15:59 +00:00
|
|
|
|
|
|
|
Ok(file)
|
|
|
|
}
|
2021-07-28 10:05:11 +00:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use super::*;
|
2022-04-06 15:00:29 +00:00
|
|
|
use anyhow::Error;
|
2021-07-28 10:05:11 +00:00
|
|
|
|
|
|
|
struct StorageDesc {
|
|
|
|
address: u16,
|
|
|
|
pvoltag: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
fn build_element_status_page(
|
|
|
|
descriptors: Vec<StorageDesc>,
|
|
|
|
trailing: &[u8],
|
|
|
|
element_type: u8,
|
|
|
|
) -> Vec<u8> {
|
2022-04-06 15:00:29 +00:00
|
|
|
let descs: Vec<Vec<u8>> = descriptors
|
|
|
|
.iter()
|
|
|
|
.map(|desc| build_storage_descriptor(desc, trailing))
|
|
|
|
.collect();
|
2021-07-28 10:05:11 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-04-06 15:00:29 +00:00
|
|
|
fn build_storage_descriptor(desc: &StorageDesc, trailing: &[u8]) -> Vec<u8> {
|
2021-07-28 10:05:11 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-04-06 15:00:29 +00:00
|
|
|
res.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0x80]);
|
2021-07-28 10:05:11 +00:00
|
|
|
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()),
|
|
|
|
},
|
|
|
|
];
|
2022-04-06 15:00:29 +00:00
|
|
|
let test_data = build_element_status_page(descs, &[0, 0, 0, 0, 0], 0x2);
|
2021-07-28 10:05:11 +00:00
|
|
|
let page = decode_element_status_page(&test_data, 0)?;
|
|
|
|
assert_eq!(page.storage_slots.len(), 2);
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|