2021-09-13 09:54:24 +00:00
|
|
|
use std::collections::HashSet;
|
|
|
|
|
|
|
|
use anyhow::{bail, Error};
|
|
|
|
use bitflags::bitflags;
|
|
|
|
use endian_trait::Endian;
|
|
|
|
use serde::{Serialize, Deserialize};
|
|
|
|
use serde_json::Value;
|
|
|
|
|
2021-10-08 09:19:37 +00:00
|
|
|
use proxmox_uuid::Uuid;
|
2021-09-13 09:54:24 +00:00
|
|
|
|
|
|
|
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 {
|
2021-10-08 09:19:37 +00:00
|
|
|
let uuid = *Uuid::generate()
|
2021-09-13 09:54:24 +00:00
|
|
|
.into_inner();
|
|
|
|
Self {
|
|
|
|
magic: PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0,
|
|
|
|
content_magic,
|
|
|
|
uuid,
|
2021-10-08 09:19:37 +00:00
|
|
|
ctime: proxmox_time::epoch_i64(),
|
2021-09-13 09:54:24 +00:00
|
|
|
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 {
|
2021-12-30 11:57:37 +00:00
|
|
|
let slots: Value = SLOT_ARRAY_SCHEMA.parse_property_string(slots)?;
|
2021-09-13 09:54:24 +00:00
|
|
|
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(())
|
|
|
|
}
|
|
|
|
}
|