tape: add tape device driver

This commit is contained in:
Dietmar Maurer 2020-12-07 08:27:15 +01:00
parent 2e7014e31d
commit fa9c9be737
8 changed files with 1599 additions and 0 deletions

View File

@ -0,0 +1,39 @@
use ::serde::{Deserialize, Serialize};
use proxmox::api::api;
#[api()]
#[derive(Debug,Serialize,Deserialize)]
#[serde(rename_all = "kebab-case")]
/// Kind of devive
pub enum DeviceKind {
/// Tape changer (Autoloader, Robot)
Changer,
/// Normal SCSI tape device
Tape,
}
#[api(
properties: {
kind: {
type: DeviceKind,
},
},
)]
#[derive(Debug,Serialize,Deserialize)]
/// Tape device information
pub struct TapeDeviceInfo {
pub kind: DeviceKind,
/// Path to the linux device node
pub path: String,
/// Serial number (autodetected)
pub serial: String,
/// Vendor (autodetected)
pub vendor: String,
/// Model (autodetected)
pub model: String,
/// Device major number
pub major: u32,
/// Device minor number
pub minor: u32,
}

View File

@ -1,5 +1,8 @@
//! Types for tape backup API
mod device;
pub use device::*;
mod drive;
pub use drive::*;

View File

@ -0,0 +1,232 @@
use std::path::{Path, PathBuf};
use std::collections::HashMap;
use anyhow::{bail, Error};
use crate::{
api2::types::{
DeviceKind,
TapeDeviceInfo,
},
tools::fs::scan_subdir,
};
/// List linux tape changer devices
pub fn linux_tape_changer_list() -> Vec<TapeDeviceInfo> {
lazy_static::lazy_static!{
static ref SCSI_GENERIC_NAME_REGEX: regex::Regex =
regex::Regex::new(r"^sg\d+$").unwrap();
}
let mut list = Vec::new();
let dir_iter = match scan_subdir(
libc::AT_FDCWD,
"/sys/class/scsi_generic",
&SCSI_GENERIC_NAME_REGEX)
{
Err(_) => return list,
Ok(iter) => iter,
};
for item in dir_iter {
let item = match item {
Err(_) => continue,
Ok(item) => item,
};
let name = item.file_name().to_str().unwrap().to_string();
let mut sys_path = PathBuf::from("/sys/class/scsi_generic");
sys_path.push(&name);
let device = match udev::Device::from_syspath(&sys_path) {
Err(_) => continue,
Ok(device) => device,
};
let devnum = match device.devnum() {
None => continue,
Some(devnum) => devnum,
};
let parent = match device.parent() {
None => continue,
Some(parent) => parent,
};
match parent.attribute_value("type") {
Some(type_osstr) => {
if type_osstr != "8" {
continue;
}
}
_ => { continue; }
}
// let mut test_path = sys_path.clone();
// test_path.push("device/scsi_changer");
// if !test_path.exists() { continue; }
let _dev_path = match device.devnode().map(Path::to_owned) {
None => continue,
Some(dev_path) => dev_path,
};
let serial = match device.property_value("ID_SCSI_SERIAL")
.map(std::ffi::OsString::from)
.and_then(|s| if let Ok(s) = s.into_string() { Some(s) } else { None })
{
None => continue,
Some(serial) => serial,
};
let vendor = device.property_value("ID_VENDOR")
.map(std::ffi::OsString::from)
.and_then(|s| if let Ok(s) = s.into_string() { Some(s) } else { None })
.unwrap_or(String::from("unknown"));
let model = device.property_value("ID_MODEL")
.map(std::ffi::OsString::from)
.and_then(|s| if let Ok(s) = s.into_string() { Some(s) } else { None })
.unwrap_or(String::from("unknown"));
let dev_path = format!("/dev/tape/by-id/scsi-{}", serial);
if PathBuf::from(&dev_path).exists() {
list.push(TapeDeviceInfo {
kind: DeviceKind::Changer,
path: dev_path,
serial,
vendor,
model,
major: unsafe { libc::major(devnum) },
minor: unsafe { libc::minor(devnum) },
});
}
}
list
}
/// List linux tape devices (non-rewinding)
pub fn linux_tape_device_list() -> Vec<TapeDeviceInfo> {
lazy_static::lazy_static!{
static ref NST_TAPE_NAME_REGEX: regex::Regex =
regex::Regex::new(r"^nst\d+$").unwrap();
}
let mut list = Vec::new();
let dir_iter = match scan_subdir(
libc::AT_FDCWD,
"/sys/class/scsi_tape",
&NST_TAPE_NAME_REGEX)
{
Err(_) => return list,
Ok(iter) => iter,
};
for item in dir_iter {
let item = match item {
Err(_) => continue,
Ok(item) => item,
};
let name = item.file_name().to_str().unwrap().to_string();
let mut sys_path = PathBuf::from("/sys/class/scsi_tape");
sys_path.push(&name);
let device = match udev::Device::from_syspath(&sys_path) {
Err(_) => continue,
Ok(device) => device,
};
let devnum = match device.devnum() {
None => continue,
Some(devnum) => devnum,
};
let _dev_path = match device.devnode().map(Path::to_owned) {
None => continue,
Some(dev_path) => dev_path,
};
let serial = match device.property_value("ID_SCSI_SERIAL")
.map(std::ffi::OsString::from)
.and_then(|s| if let Ok(s) = s.into_string() { Some(s) } else { None })
{
None => continue,
Some(serial) => serial,
};
let vendor = device.property_value("ID_VENDOR")
.map(std::ffi::OsString::from)
.and_then(|s| if let Ok(s) = s.into_string() { Some(s) } else { None })
.unwrap_or(String::from("unknown"));
let model = device.property_value("ID_MODEL")
.map(std::ffi::OsString::from)
.and_then(|s| if let Ok(s) = s.into_string() { Some(s) } else { None })
.unwrap_or(String::from("unknown"));
let dev_path = format!("/dev/tape/by-id/scsi-{}-nst", serial);
if PathBuf::from(&dev_path).exists() {
list.push(TapeDeviceInfo {
kind: DeviceKind::Tape,
path: dev_path,
serial,
vendor,
model,
major: unsafe { libc::major(devnum) },
minor: unsafe { libc::minor(devnum) },
});
}
}
list
}
/// Test if path is a linux tape device
pub fn lookup_drive<'a>(
drives: &'a[TapeDeviceInfo],
path: &str,
) -> Option<&'a TapeDeviceInfo> {
if let Ok(stat) = nix::sys::stat::stat(path) {
let major = unsafe { libc::major(stat.st_rdev) };
let minor = unsafe { libc::minor(stat.st_rdev) };
drives.iter().find(|d| d.major == major && d.minor == minor)
} else {
None
}
}
/// Make sure path is a linux tape device
pub fn check_drive_path(
drives: &[TapeDeviceInfo],
path: &str,
) -> Result<(), Error> {
if lookup_drive(drives, path).is_none() {
bail!("path '{}' is not a linux (non-rewinding) tape device", path);
}
Ok(())
}
// shell completion helper
/// List changer device paths
pub fn complete_changer_path(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
linux_tape_changer_list().iter().map(|v| v.path.clone()).collect()
}
/// List tape device paths
pub fn complete_drive_path(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
linux_tape_device_list().iter().map(|v| v.path.clone()).collect()
}

View File

@ -0,0 +1,153 @@
//! Linux Magnetic Tape Driver ioctl definitions
//!
//! from: /usr/include/x86_64-linux-gnu/sys/mtio.h
//!
//! also see: man 4 st
#[repr(C)]
pub struct mtop {
pub mt_op: MTCmd, /* Operations defined below. */
pub mt_count: libc::c_int, /* How many of them. */
}
#[repr(i16)]
#[allow(dead_code)] // do not warn about unused command
pub enum MTCmd {
MTRESET = 0, /* +reset drive in case of problems */
MTFSF = 1, /* forward space over FileMark,
* position at first record of next file
*/
MTBSF = 2, /* backward space FileMark (position before FM) */
MTFSR = 3, /* forward space record */
MTBSR = 4, /* backward space record */
MTWEOF = 5, /* write an end-of-file record (mark) */
MTREW = 6, /* rewind */
MTOFFL = 7, /* rewind and put the drive offline (eject?) */
MTNOP = 8, /* no op, set status only (read with MTIOCGET) */
MTRETEN = 9, /* retension tape */
MTBSFM = 10, /* +backward space FileMark, position at FM */
MTFSFM = 11, /* +forward space FileMark, position at FM */
MTEOM = 12, /* goto end of recorded media (for appending files).
* MTEOM positions after the last FM, ready for
* appending another file.
*/
MTERASE = 13, /* erase tape -- be careful! */
MTRAS1 = 14, /* run self test 1 (nondestructive) */
MTRAS2 = 15, /* run self test 2 (destructive) */
MTRAS3 = 16, /* reserved for self test 3 */
MTSETBLK = 20, /* set block length (SCSI) */
MTSETDENSITY = 21, /* set tape density (SCSI) */
MTSEEK = 22, /* seek to block (Tandberg, etc.) */
MTTELL = 23, /* tell block (Tandberg, etc.) */
MTSETDRVBUFFER = 24,/* set the drive buffering according to SCSI-2 */
/* ordinary buffered operation with code 1 */
MTFSS = 25, /* space forward over setmarks */
MTBSS = 26, /* space backward over setmarks */
MTWSM = 27, /* write setmarks */
MTLOCK = 28, /* lock the drive door */
MTUNLOCK = 29, /* unlock the drive door */
MTLOAD = 30, /* execute the SCSI load command */
MTUNLOAD = 31, /* execute the SCSI unload command */
MTCOMPRESSION = 32, /* control compression with SCSI mode page 15 */
MTSETPART = 33, /* Change the active tape partition */
MTMKPART = 34, /* Format the tape with one or two partitions */
MTWEOFI = 35, /* write an end-of-file record (mark) in immediate mode */
}
//#define MTIOCTOP _IOW('m', 1, struct mtop) /* Do a mag tape op. */
nix::ioctl_write_ptr!(mtioctop, b'm', 1, mtop);
// from: /usr/include/x86_64-linux-gnu/sys/mtio.h
#[derive(Default, Debug)]
#[repr(C)]
pub struct mtget {
pub mt_type: libc::c_long, /* Type of magtape device. */
pub mt_resid: libc::c_long, /* Residual count: (not sure)
number of bytes ignored, or
number of files not skipped, or
number of records not skipped. */
/* The following registers are device dependent. */
pub mt_dsreg: libc::c_long, /* Status register. */
pub mt_gstat: libc::c_long, /* Generic (device independent) status. */
pub mt_erreg: libc::c_long, /* Error register. */
/* The next two fields are not always used. */
pub mt_fileno: i32 , /* Number of current file on tape. */
pub mt_blkno: i32, /* Current block number. */
}
//#define MTIOCGET _IOR('m', 2, struct mtget) /* Get tape status. */
nix::ioctl_read!(mtiocget, b'm', 2, mtget);
#[repr(C)]
#[allow(dead_code)]
pub struct mtpos {
pub mt_blkno: libc::c_long, /* current block number */
}
//#define MTIOCPOS _IOR('m', 3, struct mtpos) /* Get tape position.*/
nix::ioctl_read!(mtiocpos, b'm', 3, mtpos);
pub const MT_ST_BLKSIZE_MASK: libc::c_long = 0x0ffffff;
pub const MT_ST_BLKSIZE_SHIFT: usize = 0;
pub const MT_ST_DENSITY_MASK: libc::c_long = 0xff000000;
pub const MT_ST_DENSITY_SHIFT: usize = 24;
pub const MT_TYPE_ISSCSI1: libc::c_long = 0x71; /* Generic ANSI SCSI-1 tape unit. */
pub const MT_TYPE_ISSCSI2: libc::c_long = 0x72; /* Generic ANSI SCSI-2 tape unit. */
// Generic Mag Tape (device independent) status macros for examining mt_gstat -- HP-UX compatible
// from: /usr/include/x86_64-linux-gnu/sys/mtio.h
bitflags::bitflags!{
pub struct GMTStatusFlags: libc::c_long {
const EOF = 0x80000000;
const BOT = 0x40000000;
const EOT = 0x20000000;
const SM = 0x10000000; /* DDS setmark */
const EOD = 0x08000000; /* DDS EOD */
const WR_PROT = 0x04000000;
const ONLINE = 0x01000000;
const D_6250 = 0x00800000;
const D_1600 = 0x00400000;
const D_800 = 0x00200000;
const DRIVE_OPEN = 0x00040000; /* Door open (no tape). */
const IM_REP_EN = 0x00010000; /* Immediate report mode.*/
const END_OF_STREAM = 0b00000001;
}
}
#[repr(i32)]
#[allow(non_camel_case_types, dead_code)]
pub enum SetDrvBufferCmd {
MT_ST_BOOLEANS = 0x10000000,
MT_ST_SETBOOLEANS = 0x30000000,
MT_ST_CLEARBOOLEANS = 0x40000000,
MT_ST_WRITE_THRESHOLD = 0x20000000,
MT_ST_DEF_BLKSIZE = 0x50000000,
MT_ST_DEF_OPTIONS = 0x60000000,
MT_ST_SET_TIMEOUT = 0x70000000,
MT_ST_SET_LONG_TIMEOUT = 0x70100000,
MT_ST_SET_CLN = 0x80000000u32 as i32,
}
bitflags::bitflags!{
pub struct SetDrvBufferOptions: i32 {
const MT_ST_BUFFER_WRITES = 0x1;
const MT_ST_ASYNC_WRITES = 0x2;
const MT_ST_READ_AHEAD = 0x4;
const MT_ST_DEBUGGING = 0x8;
const MT_ST_TWO_FM = 0x10;
const MT_ST_FAST_MTEOM = 0x20;
const MT_ST_AUTO_LOCK = 0x40;
const MT_ST_DEF_WRITES = 0x80;
const MT_ST_CAN_BSR = 0x100;
const MT_ST_NO_BLKLIMS = 0x200;
const MT_ST_CAN_PARTITIONS = 0x400;
const MT_ST_SCSI2LOGICAL = 0x800;
const MT_ST_SYSV = 0x1000;
const MT_ST_NOWAIT = 0x2000;
const MT_ST_SILI = 0x4000;
}
}

View File

@ -0,0 +1,446 @@
use std::fs::{OpenOptions, File};
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
use std::convert::TryFrom;
use anyhow::{bail, format_err, Error};
use nix::fcntl::{fcntl, FcntlArg, OFlag};
use proxmox::sys::error::SysResult;
use proxmox::tools::Uuid;
use crate::{
tape::{
TapeRead,
TapeWrite,
drive::{
LinuxTapeDrive,
TapeDriver,
linux_mtio::*,
},
file_formats::{
PROXMOX_TAPE_BLOCK_SIZE,
MediaSetLabel,
MediaContentHeader,
PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0,
},
helpers::{
BlockedReader,
BlockedWriter,
},
}
};
#[derive(Debug)]
pub enum TapeDensity {
None, // no tape loaded
LTO2,
LTO3,
LTO4,
LTO5,
LTO6,
LTO7,
LTO7M8,
LTO8,
}
impl TryFrom<u8> for TapeDensity {
type Error = Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
let density = match value {
0x00 => TapeDensity::None,
0x42 => TapeDensity::LTO2,
0x44 => TapeDensity::LTO3,
0x46 => TapeDensity::LTO4,
0x58 => TapeDensity::LTO5,
0x5a => TapeDensity::LTO6,
0x5c => TapeDensity::LTO7,
0x5d => TapeDensity::LTO7M8,
0x5e => TapeDensity::LTO8,
_ => bail!("unknown tape density code 0x{:02x}", value),
};
Ok(density)
}
}
#[derive(Debug)]
pub struct DriveStatus {
pub blocksize: u32,
pub density: TapeDensity,
pub status: GMTStatusFlags,
pub file_number: i32,
pub block_number: i32,
}
impl DriveStatus {
pub fn tape_is_ready(&self) -> bool {
self.status.contains(GMTStatusFlags::ONLINE) &&
!self.status.contains(GMTStatusFlags::DRIVE_OPEN)
}
}
impl LinuxTapeDrive {
/// This needs to lock the drive
pub fn open(&self) -> Result<LinuxTapeHandle, Error> {
let file = OpenOptions::new()
.read(true)
.write(true)
.custom_flags(libc::O_NONBLOCK)
.open(&self.path)?;
// clear O_NONBLOCK from now on.
let flags = fcntl(file.as_raw_fd(), FcntlArg::F_GETFL)
.into_io_result()?;
let mut flags = OFlag::from_bits_truncate(flags);
flags.remove(OFlag::O_NONBLOCK);
fcntl(file.as_raw_fd(), FcntlArg::F_SETFL(flags))
.into_io_result()?;
if !tape_is_linux_tape_device(&file) {
bail!("file {:?} is not a linux tape device", self.path);
}
let handle = LinuxTapeHandle { drive_name: self.name.clone(), file };
let drive_status = handle.get_drive_status()?;
println!("drive status: {:?}", drive_status);
if !drive_status.tape_is_ready() {
bail!("tape not ready (no tape loaded)");
}
if drive_status.blocksize == 0 {
eprintln!("device is variable block size");
} else {
if drive_status.blocksize != PROXMOX_TAPE_BLOCK_SIZE as u32 {
eprintln!("device is in fixed block size mode with wrong size ({} bytes)", drive_status.blocksize);
eprintln!("trying to set variable block size mode...");
if handle.set_block_size(0).is_err() {
bail!("set variable block size mod failed - device uses wrong blocksize.");
}
} else {
eprintln!("device is in fixed block size mode ({} bytes)", drive_status.blocksize);
}
}
// Only root can seth driver options, so we cannot
// handle.set_default_options()?;
Ok(handle)
}
}
pub struct LinuxTapeHandle {
drive_name: String,
file: File,
//_lock: File,
}
impl LinuxTapeHandle {
/// Return the drive name (useful for log and debug)
pub fn dive_name(&self) -> &str {
&self.drive_name
}
/// Set all options we need/want
pub fn set_default_options(&self) -> Result<(), Error> {
let mut opts = SetDrvBufferOptions::empty();
// fixme: ? man st(4) claims we need to clear this for reliable multivolume
opts.set(SetDrvBufferOptions::MT_ST_BUFFER_WRITES, true);
// fixme: ?man st(4) claims we need to clear this for reliable multivolume
opts.set(SetDrvBufferOptions::MT_ST_ASYNC_WRITES, true);
opts.set(SetDrvBufferOptions::MT_ST_READ_AHEAD, true);
self.set_drive_buffer_options(opts)
}
/// call MTSETDRVBUFFER to set boolean options
///
/// Note: this uses MT_ST_BOOLEANS, so missing options are cleared!
pub fn set_drive_buffer_options(&self, opts: SetDrvBufferOptions) -> Result<(), Error> {
let cmd = mtop {
mt_op: MTCmd::MTSETDRVBUFFER,
mt_count: (SetDrvBufferCmd::MT_ST_BOOLEANS as i32) | opts.bits(),
};
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("MTSETDRVBUFFER options failed - {}", err))?;
Ok(())
}
/// This flushes the driver's buffer as a side effect. Should be
/// used before reading status with MTIOCGET.
fn mtnop(&self) -> Result<(), Error> {
let cmd = mtop { mt_op: MTCmd::MTNOP, mt_count: 1, };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("MTNOP failed - {}", err))?;
Ok(())
}
/// Set tape compression feature
pub fn set_compression(&self, on: bool) -> Result<(), Error> {
let cmd = mtop { mt_op: MTCmd::MTCOMPRESSION, mt_count: if on { 1 } else { 0 } };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("set compression to {} failed - {}", on, err))?;
Ok(())
}
/// Write a single EOF mark
pub fn write_eof_mark(&self) -> Result<(), Error> {
tape_write_eof_mark(&self.file)?;
Ok(())
}
/// Set the drive's block length to the value specified.
///
/// A block length of zero sets the drive to variable block
/// size mode.
pub fn set_block_size(&self, block_length: usize) -> Result<(), Error> {
if block_length > 256*1024*1024 {
bail!("block_length too large (> max linux scsii block length)");
}
let cmd = mtop { mt_op: MTCmd::MTSETBLK, mt_count: block_length as i32 };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("MTSETBLK failed - {}", err))?;
Ok(())
}
/// Get Tape configuration with MTIOCGET ioctl
pub fn get_drive_status(&self) -> Result<DriveStatus, Error> {
self.mtnop()?;
let mut status = mtget::default();
if let Err(err) = unsafe { mtiocget(self.file.as_raw_fd(), &mut status) } {
bail!("MTIOCGET failed - {}", err);
}
println!("{:?}", status);
let gmt = GMTStatusFlags::from_bits_truncate(status.mt_gstat);
let blocksize;
if status.mt_type == MT_TYPE_ISSCSI1 || status.mt_type == MT_TYPE_ISSCSI2 {
blocksize = ((status.mt_dsreg & MT_ST_BLKSIZE_MASK) >> MT_ST_BLKSIZE_SHIFT) as u32;
} else {
bail!("got unsupported tape type {}", status.mt_type);
}
let density = ((status.mt_dsreg & MT_ST_DENSITY_MASK) >> MT_ST_DENSITY_SHIFT) as u8;
let density = TapeDensity::try_from(density)?;
Ok(DriveStatus {
blocksize,
density,
status: gmt,
file_number: status.mt_fileno,
block_number: status.mt_blkno,
})
}
}
impl TapeDriver for LinuxTapeHandle {
fn sync(&mut self) -> Result<(), Error> {
println!("SYNC/FLUSH TAPE");
// MTWEOF with count 0 => flush
let cmd = mtop { mt_op: MTCmd::MTWEOF, mt_count: 0 };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| proxmox::io_format_err!("MT sync failed - {}", err))?;
Ok(())
}
/// Go to the end of the recorded media (for appending files).
fn move_to_eom(&mut self) -> Result<(), Error> {
let cmd = mtop { mt_op: MTCmd::MTEOM, mt_count: 1, };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("MTEOM failed - {}", err))?;
Ok(())
}
fn rewind(&mut self) -> Result<(), Error> {
let cmd = mtop { mt_op: MTCmd::MTREW, mt_count: 1, };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("tape rewind failed - {}", err))?;
Ok(())
}
fn current_file_number(&mut self) -> Result<usize, Error> {
let mut status = mtget::default();
self.mtnop()?;
if let Err(err) = unsafe { mtiocget(self.file.as_raw_fd(), &mut status) } {
bail!("current_file_number MTIOCGET failed - {}", err);
}
if status.mt_fileno < 0 {
bail!("current_file_number failed (got {})", status.mt_fileno);
}
Ok(status.mt_fileno as usize)
}
fn erase_media(&mut self, fast: bool) -> Result<(), Error> {
self.rewind()?; // important - erase from BOT
let cmd = mtop { mt_op: MTCmd::MTERASE, mt_count: if fast { 0 } else { 1 } };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("MTERASE failed - {}", err))?;
Ok(())
}
fn read_next_file<'a>(&'a mut self) -> Result<Option<Box<dyn TapeRead + 'a>>, std::io::Error> {
match BlockedReader::open(&mut self.file)? {
Some(reader) => Ok(Some(Box::new(reader))),
None => Ok(None),
}
}
fn write_file<'a>(&'a mut self) -> Result<Box<dyn TapeWrite + 'a>, std::io::Error> {
let handle = TapeWriterHandle {
writer: BlockedWriter::new(&mut self.file),
};
Ok(Box::new(handle))
}
fn write_media_set_label(&mut self, media_set_label: &MediaSetLabel) -> Result<Uuid, Error> {
let file_number = self.current_file_number()?;
if file_number != 1 {
bail!("write_media_set_label failed - got wrong file number ({} != 1)", file_number);
}
let mut handle = TapeWriterHandle {
writer: BlockedWriter::new(&mut self.file),
};
let raw = serde_json::to_string_pretty(&serde_json::to_value(media_set_label)?)?;
let header = MediaContentHeader::new(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, raw.len() as u32);
handle.write_header(&header, raw.as_bytes())?;
handle.finish(false)?;
self.sync()?; // sync data to tape
Ok(Uuid::from(header.uuid))
}
/// Rewind and put the drive off line (Eject media).
fn eject_media(&mut self) -> Result<(), Error> {
let cmd = mtop { mt_op: MTCmd::MTOFFL, mt_count: 1 };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("MTOFFL failed - {}", err))?;
Ok(())
}
}
/// Write a single EOF mark without flushing buffers
fn tape_write_eof_mark(file: &File) -> Result<(), std::io::Error> {
println!("WRITE EOF MARK");
let cmd = mtop { mt_op: MTCmd::MTWEOFI, mt_count: 1 };
unsafe {
mtioctop(file.as_raw_fd(), &cmd)
}.map_err(|err| proxmox::io_format_err!("MTWEOFI failed - {}", err))?;
Ok(())
}
fn tape_is_linux_tape_device(file: &File) -> bool {
let devnum = match nix::sys::stat::fstat(file.as_raw_fd()) {
Ok(stat) => stat.st_rdev,
_ => return false,
};
let major = unsafe { libc::major(devnum) };
let minor = unsafe { libc::minor(devnum) };
if major != 9 { return false; } // The st driver uses major device number 9
if (minor & 128) == 0 {
eprintln!("Detected rewinding tape. Please use non-rewinding tape devices (/dev/nstX).");
return false;
}
true
}
/// like BlockedWriter, but writes EOF mark on finish
pub struct TapeWriterHandle<'a> {
writer: BlockedWriter<&'a mut File>,
}
impl TapeWrite for TapeWriterHandle<'_> {
fn write_all(&mut self, data: &[u8]) -> Result<bool, std::io::Error> {
self.writer.write_all(data)
}
fn bytes_written(&self) -> usize {
self.writer.bytes_written()
}
fn finish(&mut self, incomplete: bool) -> Result<bool, std::io::Error> {
println!("FINISH TAPE HANDLE");
let leof = self.writer.finish(incomplete)?;
tape_write_eof_mark(self.writer.writer_ref_mut())?;
Ok(leof)
}
fn logical_end_of_media(&self) -> bool {
self.writer.logical_end_of_media()
}
}

299
src/tape/drive/mod.rs Normal file
View File

@ -0,0 +1,299 @@
mod virtual_tape;
mod linux_mtio;
mod linux_tape;
mod linux_list_drives;
pub use linux_list_drives::*;
use anyhow::{bail, format_err, Error};
use ::serde::{Deserialize, Serialize};
use proxmox::tools::Uuid;
use proxmox::tools::io::ReadExt;
use proxmox::api::section_config::SectionConfigData;
use crate::{
api2::types::{
VirtualTapeDrive,
LinuxTapeDrive,
},
tape::{
TapeWrite,
TapeRead,
file_formats::{
PROXMOX_BACKUP_DRIVE_LABEL_MAGIC_1_0,
PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0,
DriveLabel,
MediaSetLabel,
MediaContentHeader,
},
changer::{
MediaChange,
ChangeMediaEmail,
},
},
};
#[derive(Serialize,Deserialize)]
pub struct MediaLabelInfo {
pub label: DriveLabel,
pub label_uuid: Uuid,
#[serde(skip_serializing_if="Option::is_none")]
pub media_set_label: Option<(MediaSetLabel, Uuid)>
}
/// Tape driver interface
pub trait TapeDriver {
/// Flush all data to the tape
fn sync(&mut self) -> Result<(), Error>;
/// Rewind the tape
fn rewind(&mut self) -> Result<(), Error>;
/// Move to end of recorded data
///
/// We assume this flushes the tape write buffer.
fn move_to_eom(&mut self) -> Result<(), Error>;
/// Current file number
fn current_file_number(&mut self) -> Result<usize, Error>;
/// Completely erase the media
fn erase_media(&mut self, fast: bool) -> Result<(), Error>;
/// Read/Open the next file
fn read_next_file<'a>(&'a mut self) -> Result<Option<Box<dyn TapeRead + 'a>>, std::io::Error>;
/// Write/Append a new file
fn write_file<'a>(&'a mut self) -> Result<Box<dyn TapeWrite + 'a>, std::io::Error>;
/// Write label to tape (erase tape content)
///
/// This returns the MediaContentHeader uuid (not the media uuid).
fn label_tape(&mut self, label: &DriveLabel) -> Result<Uuid, Error> {
self.rewind()?;
self.erase_media(true)?;
let raw = serde_json::to_string_pretty(&serde_json::to_value(&label)?)?;
let header = MediaContentHeader::new(PROXMOX_BACKUP_DRIVE_LABEL_MAGIC_1_0, raw.len() as u32);
let content_uuid = header.content_uuid();
{
let mut writer = self.write_file()?;
writer.write_header(&header, raw.as_bytes())?;
writer.finish(false)?;
}
self.sync()?; // sync data to tape
Ok(content_uuid)
}
/// Write the media set label to tape
///
/// This returns the MediaContentHeader uuid (not the media uuid).
fn write_media_set_label(&mut self, media_set_label: &MediaSetLabel) -> Result<Uuid, Error>;
/// Read the media label
///
/// This tries to read both media labels (label and media_set_label).
fn read_label(&mut self) -> Result<Option<MediaLabelInfo>, Error> {
self.rewind()?;
let (label, label_uuid) = {
let mut reader = match self.read_next_file()? {
None => return Ok(None), // tape is empty
Some(reader) => reader,
};
let header: MediaContentHeader = unsafe { reader.read_le_value()? };
header.check(PROXMOX_BACKUP_DRIVE_LABEL_MAGIC_1_0, 1, 64*1024)?;
let data = reader.read_exact_allocated(header.size as usize)?;
let label: DriveLabel = serde_json::from_slice(&data)
.map_err(|err| format_err!("unable to parse drive label - {}", err))?;
// make sure we read the EOF marker
if reader.skip_to_end()? != 0 {
bail!("got unexpected data after label");
}
(label, Uuid::from(header.uuid))
};
let mut info = MediaLabelInfo { label, label_uuid, media_set_label: None };
// try to read MediaSet label
let mut reader = match self.read_next_file()? {
None => return Ok(Some(info)),
Some(reader) => reader,
};
let header: MediaContentHeader = unsafe { reader.read_le_value()? };
header.check(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, 1, 64*1024)?;
let data = reader.read_exact_allocated(header.size as usize)?;
let media_set_label: MediaSetLabel = serde_json::from_slice(&data)
.map_err(|err| format_err!("unable to parse media set label - {}", err))?;
// make sure we read the EOF marker
if reader.skip_to_end()? != 0 {
bail!("got unexpected data after media set label");
}
info.media_set_label = Some((media_set_label, Uuid::from(header.uuid)));
Ok(Some(info))
}
/// Eject media
fn eject_media(&mut self) -> Result<(), Error>;
}
/// Get the media changer (name + MediaChange) associated with a tape drie.
///
/// If allow_email is set, returns an ChangeMediaEmail instance for
/// standalone tape drives (changer name set to "").
pub fn media_changer(
config: &SectionConfigData,
drive: &str,
allow_email: bool,
) -> Result<(Box<dyn MediaChange>, String), Error> {
match config.sections.get(drive) {
Some((section_type_name, config)) => {
match section_type_name.as_ref() {
"virtual" => {
let tape = VirtualTapeDrive::deserialize(config)?;
Ok((Box::new(tape), drive.to_string()))
}
"linux" => {
let tape = LinuxTapeDrive::deserialize(config)?;
match tape.changer {
Some(ref changer_name) => {
let changer_name = changer_name.to_string();
Ok((Box::new(tape), changer_name))
}
None => {
if !allow_email {
bail!("drive '{}' has no changer device", drive);
}
let to = "root@localhost"; // fixme
let changer = ChangeMediaEmail::new(drive, to);
Ok((Box::new(changer), String::new()))
},
}
}
_ => bail!("drive type '{}' not implemented!"),
}
}
None => {
bail!("no such drive '{}'", drive);
}
}
}
pub fn open_drive(
config: &SectionConfigData,
drive: &str,
) -> Result<Box<dyn TapeDriver>, Error> {
match config.sections.get(drive) {
Some((section_type_name, config)) => {
match section_type_name.as_ref() {
"virtual" => {
let tape = VirtualTapeDrive::deserialize(config)?;
let handle = tape.open()
.map_err(|err| format_err!("open drive '{}' ({}) failed - {}", drive, tape.path, err))?;
Ok(Box::new(handle))
}
"linux" => {
let tape = LinuxTapeDrive::deserialize(config)?;
let handle = tape.open()
.map_err(|err| format_err!("open drive '{}' ({}) failed - {}", drive, tape.path, err))?;
Ok(Box::new(handle))
}
_ => bail!("drive type '{}' not implemented!"),
}
}
None => {
bail!("no such drive '{}'", drive);
}
}
}
/// Requests a specific 'media' to be inserted into 'drive'. Within a
/// loop, this then tries to read the media label and waits until it
/// finds the requested media.
///
/// Returns a handle to the opened drive and the media labels.
pub fn request_and_load_media(
config: &SectionConfigData,
drive: &str,
label: &DriveLabel,
) -> Result<(
Box<dyn TapeDriver>,
MediaLabelInfo,
), Error> {
match config.sections.get(drive) {
Some((section_type_name, config)) => {
match section_type_name.as_ref() {
"virtual" => {
let mut drive = VirtualTapeDrive::deserialize(config)?;
let changer_id = label.changer_id.clone();
drive.load_media(&changer_id)?;
let mut handle = drive.open()?;
if let Ok(Some(info)) = handle.read_label() {
println!("found media label {} ({})", info.label.changer_id, info.label.uuid.to_string());
if info.label.uuid == label.uuid {
return Ok((Box::new(handle), info));
}
}
bail!("read label failed (label all tapes first)");
}
"linux" => {
let tape = LinuxTapeDrive::deserialize(config)?;
let id = label.changer_id.clone();
println!("Please insert media '{}' into drive '{}'", id, drive);
loop {
let mut handle = match tape.open() {
Ok(handle) => handle,
Err(_) => {
eprintln!("tape open failed - test again in 5 secs");
std::thread::sleep(std::time::Duration::from_millis(5_000));
continue;
}
};
if let Ok(Some(info)) = handle.read_label() {
println!("found media label {} ({})", info.label.changer_id, info.label.uuid.to_string());
if info.label.uuid == label.uuid {
return Ok((Box::new(handle), info));
}
}
println!("read label failed - test again in 5 secs");
std::thread::sleep(std::time::Duration::from_millis(5_000));
}
}
_ => bail!("drive type '{}' not implemented!"),
}
}
None => {
bail!("no such drive '{}'", drive);
}
}
}

View File

@ -0,0 +1,424 @@
// Note: This is only for test an debug
use std::fs::File;
use std::io;
use anyhow::{bail, format_err, Error};
use serde::{Serialize, Deserialize};
use proxmox::tools::{
Uuid,
fs::{replace_file, CreateOptions},
};
use crate::{
tape::{
TapeWrite,
TapeRead,
changer::MediaChange,
drive::{
VirtualTapeDrive,
TapeDriver,
},
file_formats::{
MediaSetLabel,
MediaContentHeader,
PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0,
},
helpers::{
EmulateTapeReader,
EmulateTapeWriter,
BlockedReader,
BlockedWriter,
},
},
};
impl VirtualTapeDrive {
/// This needs to lock the drive
pub fn open(&self) -> Result<VirtualTapeHandle, Error> {
let mut lock_path = std::path::PathBuf::from(&self.path);
lock_path.push(".drive.lck");
let timeout = std::time::Duration::new(10, 0);
let lock = proxmox::tools::fs::open_file_locked(&lock_path, timeout, true)?;
Ok(VirtualTapeHandle {
_lock: lock,
max_size: self.max_size.unwrap_or(64*1024*1024),
path: std::path::PathBuf::from(&self.path),
})
}
}
#[derive(Serialize,Deserialize)]
struct VirtualTapeStatus {
name: String,
pos: usize,
}
#[derive(Serialize,Deserialize)]
struct VirtualDriveStatus {
current_tape: Option<VirtualTapeStatus>,
}
#[derive(Serialize,Deserialize)]
struct TapeIndex {
files: usize,
}
pub struct VirtualTapeHandle {
path: std::path::PathBuf,
max_size: usize,
_lock: File,
}
impl VirtualTapeHandle {
pub fn insert_tape(&self, _tape_filename: &str) {
unimplemented!();
}
pub fn eject_tape(&self) {
unimplemented!();
}
fn status_file_path(&self) -> std::path::PathBuf {
let mut path = self.path.clone();
path.push("drive-status.json");
path
}
fn tape_index_path(&self, tape_name: &str) -> std::path::PathBuf {
let mut path = self.path.clone();
path.push(format!("tape-{}.json", tape_name));
path
}
fn tape_file_path(&self, tape_name: &str, pos: usize) -> std::path::PathBuf {
let mut path = self.path.clone();
path.push(format!("tapefile-{}-{}.json", pos, tape_name));
path
}
fn load_tape_index(&self, tape_name: &str) -> Result<TapeIndex, Error> {
let path = self.tape_index_path(tape_name);
let raw = proxmox::tools::fs::file_get_contents(&path)?;
if raw.is_empty() {
return Ok(TapeIndex { files: 0 });
}
let data: TapeIndex = serde_json::from_slice(&raw)?;
Ok(data)
}
fn store_tape_index(&self, tape_name: &str, index: &TapeIndex) -> Result<(), Error> {
let path = self.tape_index_path(tape_name);
let raw = serde_json::to_string_pretty(&serde_json::to_value(index)?)?;
let options = CreateOptions::new();
replace_file(&path, raw.as_bytes(), options)?;
Ok(())
}
fn truncate_tape(&self, tape_name: &str, pos: usize) -> Result<usize, Error> {
let mut index = self.load_tape_index(tape_name)?;
if index.files <= pos {
return Ok(index.files)
}
for i in pos..index.files {
let path = self.tape_file_path(tape_name, i);
let _ = std::fs::remove_file(path);
}
index.files = pos;
self.store_tape_index(tape_name, &index)?;
Ok(index.files)
}
fn load_status(&self) -> Result<VirtualDriveStatus, Error> {
let path = self.status_file_path();
let default = serde_json::to_value(VirtualDriveStatus {
current_tape: None,
})?;
let data = proxmox::tools::fs::file_get_json(&path, Some(default))?;
let status: VirtualDriveStatus = serde_json::from_value(data)?;
Ok(status)
}
fn store_status(&self, status: &VirtualDriveStatus) -> Result<(), Error> {
let path = self.status_file_path();
let raw = serde_json::to_string_pretty(&serde_json::to_value(status)?)?;
let options = CreateOptions::new();
replace_file(&path, raw.as_bytes(), options)?;
Ok(())
}
}
impl TapeDriver for VirtualTapeHandle {
fn sync(&mut self) -> Result<(), Error> {
Ok(()) // do nothing for now
}
fn current_file_number(&mut self) -> Result<usize, Error> {
let status = self.load_status()
.map_err(|err| format_err!("current_file_number failed: {}", err.to_string()))?;
match status.current_tape {
Some(VirtualTapeStatus { pos, .. }) => { Ok(pos)},
None => bail!("current_file_number failed: drive is empty (no tape loaded)."),
}
}
fn read_next_file(&mut self) -> Result<Option<Box<dyn TapeRead>>, io::Error> {
let mut status = self.load_status()
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
match status.current_tape {
Some(VirtualTapeStatus { ref name, ref mut pos }) => {
let index = self.load_tape_index(name)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
if *pos >= index.files {
return Ok(None); // EOM
}
let path = self.tape_file_path(name, *pos);
let file = std::fs::OpenOptions::new()
.read(true)
.open(path)?;
*pos += 1;
self.store_status(&status)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
let reader = Box::new(file);
let reader = Box::new(EmulateTapeReader::new(reader));
match BlockedReader::open(reader)? {
Some(reader) => Ok(Some(Box::new(reader))),
None => Ok(None),
}
}
None => proxmox::io_bail!("drive is empty (no tape loaded)."),
}
}
fn write_file(&mut self) -> Result<Box<dyn TapeWrite>, io::Error> {
let mut status = self.load_status()
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
match status.current_tape {
Some(VirtualTapeStatus { ref name, ref mut pos }) => {
let mut index = self.load_tape_index(name)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
for i in *pos..index.files {
let path = self.tape_file_path(name, i);
let _ = std::fs::remove_file(path);
}
let mut used_space = 0;
for i in 0..*pos {
let path = self.tape_file_path(name, i);
used_space += path.metadata()?.len() as usize;
}
index.files = *pos + 1;
self.store_tape_index(name, &index)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
let path = self.tape_file_path(name, *pos);
let file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)?;
*pos = index.files;
self.store_status(&status)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
let mut free_space = 0;
if used_space < self.max_size {
free_space = self.max_size - used_space;
}
let writer = Box::new(file);
let writer = Box::new(EmulateTapeWriter::new(writer, free_space));
let writer = Box::new(BlockedWriter::new(writer));
Ok(writer)
}
None => proxmox::io_bail!("drive is empty (no tape loaded)."),
}
}
fn move_to_eom(&mut self) -> Result<(), Error> {
let mut status = self.load_status()?;
match status.current_tape {
Some(VirtualTapeStatus { ref name, ref mut pos }) => {
let index = self.load_tape_index(name)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
*pos = index.files;
self.store_status(&status)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
Ok(())
}
None => bail!("drive is empty (no tape loaded)."),
}
}
fn rewind(&mut self) -> Result<(), Error> {
let mut status = self.load_status()?;
match status.current_tape {
Some(ref mut tape_status) => {
tape_status.pos = 0;
self.store_status(&status)?;
Ok(())
}
None => bail!("drive is empty (no tape loaded)."),
}
}
fn erase_media(&mut self, _fast: bool) -> Result<(), Error> {
let mut status = self.load_status()?;
match status.current_tape {
Some(VirtualTapeStatus { ref name, ref mut pos }) => {
*pos = self.truncate_tape(name, 0)?;
self.store_status(&status)?;
Ok(())
}
None => bail!("drive is empty (no tape loaded)."),
}
}
fn write_media_set_label(&mut self, media_set_label: &MediaSetLabel) -> Result<Uuid, Error> {
let mut status = self.load_status()?;
match status.current_tape {
Some(VirtualTapeStatus { ref name, ref mut pos }) => {
*pos = self.truncate_tape(name, 1)?;
let pos = *pos;
self.store_status(&status)?;
if pos == 0 {
bail!("media is empty (no label).");
}
if pos != 1 {
bail!("write_media_set_label: truncate failed - got wrong pos '{}'", pos);
}
let raw = serde_json::to_string_pretty(&serde_json::to_value(media_set_label)?)?;
let header = MediaContentHeader::new(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, raw.len() as u32);
{
let mut writer = self.write_file()?;
writer.write_header(&header, raw.as_bytes())?;
writer.finish(false)?;
}
Ok(Uuid::from(header.uuid))
}
None => bail!("drive is empty (no tape loaded)."),
}
}
fn eject_media(&mut self) -> Result<(), Error> {
let status = VirtualDriveStatus {
current_tape: None,
};
self.store_status(&status)
}
}
impl MediaChange for VirtualTapeHandle {
/// Try to load media
///
/// We automatically create an empty virtual tape here (if it does
/// not exist already)
fn load_media(&mut self, label: &str) -> Result<(), Error> {
let name = format!("tape-{}.json", label);
let mut path = self.path.clone();
path.push(&name);
if !path.exists() {
eprintln!("unable to find tape {} - creating file {:?}", label, path);
let index = TapeIndex { files: 0 };
self.store_tape_index(label, &index)?;
}
let status = VirtualDriveStatus {
current_tape: Some(VirtualTapeStatus {
name: label.to_string(),
pos: 0,
}),
};
self.store_status(&status)
}
fn unload_media(&mut self) -> Result<(), Error> {
self.eject_media()?;
Ok(())
}
fn eject_on_unload(&self) -> bool {
true
}
fn list_media_changer_ids(&self) -> Result<Vec<String>, Error> {
let mut list = Vec::new();
for entry in std::fs::read_dir(&self.path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension() == Some(std::ffi::OsStr::new("json")) {
if let Some(name) = path.file_stem() {
if let Some(name) = name.to_str() {
if name.starts_with("tape-") {
list.push(name[5..].to_string());
}
}
}
}
}
Ok(list)
}
}
impl MediaChange for VirtualTapeDrive {
fn load_media(&mut self, changer_id: &str) -> Result<(), Error> {
let mut handle = self.open()?;
handle.load_media(changer_id)
}
fn unload_media(&mut self) -> Result<(), Error> {
let mut handle = self.open()?;
handle.eject_media()?;
Ok(())
}
fn eject_on_unload(&self) -> bool {
true
}
fn list_media_changer_ids(&self) -> Result<Vec<String>, Error> {
let handle = self.open()?;
handle.list_media_changer_ids()
}
}

View File

@ -15,6 +15,9 @@ pub use inventory::*;
mod changer;
pub use changer::*;
mod drive;
pub use drive::*;
/// Directory path where we stora all status information
pub const MEDIA_POOL_STATUS_DIR: &str = "/var/lib/proxmox-backup/mediapool";