tape: add tape device driver
This commit is contained in:
parent
2e7014e31d
commit
fa9c9be737
39
src/api2/types/tape/device.rs
Normal file
39
src/api2/types/tape/device.rs
Normal 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,
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
//! Types for tape backup API
|
||||
|
||||
mod device;
|
||||
pub use device::*;
|
||||
|
||||
mod drive;
|
||||
pub use drive::*;
|
||||
|
||||
|
232
src/tape/drive/linux_list_drives.rs
Normal file
232
src/tape/drive/linux_list_drives.rs
Normal 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()
|
||||
}
|
153
src/tape/drive/linux_mtio.rs
Normal file
153
src/tape/drive/linux_mtio.rs
Normal 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;
|
||||
}
|
||||
}
|
446
src/tape/drive/linux_tape.rs
Normal file
446
src/tape/drive/linux_tape.rs
Normal 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
299
src/tape/drive/mod.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
424
src/tape/drive/virtual_tape.rs
Normal file
424
src/tape/drive/virtual_tape.rs
Normal 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()
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user