From fa9c9be737054fdbe1ccd6333ecd59773fedf4b9 Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Mon, 7 Dec 2020 08:27:15 +0100 Subject: [PATCH] tape: add tape device driver --- src/api2/types/tape/device.rs | 39 +++ src/api2/types/tape/mod.rs | 3 + src/tape/drive/linux_list_drives.rs | 232 +++++++++++++++ src/tape/drive/linux_mtio.rs | 153 ++++++++++ src/tape/drive/linux_tape.rs | 446 ++++++++++++++++++++++++++++ src/tape/drive/mod.rs | 299 +++++++++++++++++++ src/tape/drive/virtual_tape.rs | 424 ++++++++++++++++++++++++++ src/tape/mod.rs | 3 + 8 files changed, 1599 insertions(+) create mode 100644 src/api2/types/tape/device.rs create mode 100644 src/tape/drive/linux_list_drives.rs create mode 100644 src/tape/drive/linux_mtio.rs create mode 100644 src/tape/drive/linux_tape.rs create mode 100644 src/tape/drive/mod.rs create mode 100644 src/tape/drive/virtual_tape.rs diff --git a/src/api2/types/tape/device.rs b/src/api2/types/tape/device.rs new file mode 100644 index 00000000..11495b88 --- /dev/null +++ b/src/api2/types/tape/device.rs @@ -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, +} diff --git a/src/api2/types/tape/mod.rs b/src/api2/types/tape/mod.rs index 1c59ab0f..a6b4689b 100644 --- a/src/api2/types/tape/mod.rs +++ b/src/api2/types/tape/mod.rs @@ -1,5 +1,8 @@ //! Types for tape backup API +mod device; +pub use device::*; + mod drive; pub use drive::*; diff --git a/src/tape/drive/linux_list_drives.rs b/src/tape/drive/linux_list_drives.rs new file mode 100644 index 00000000..1b5926d1 --- /dev/null +++ b/src/tape/drive/linux_list_drives.rs @@ -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 { + + 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 { + + 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) -> Vec { + linux_tape_changer_list().iter().map(|v| v.path.clone()).collect() +} + +/// List tape device paths +pub fn complete_drive_path(_arg: &str, _param: &HashMap) -> Vec { + linux_tape_device_list().iter().map(|v| v.path.clone()).collect() +} diff --git a/src/tape/drive/linux_mtio.rs b/src/tape/drive/linux_mtio.rs new file mode 100644 index 00000000..a8158a4f --- /dev/null +++ b/src/tape/drive/linux_mtio.rs @@ -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; + } +} diff --git a/src/tape/drive/linux_tape.rs b/src/tape/drive/linux_tape.rs new file mode 100644 index 00000000..218d9820 --- /dev/null +++ b/src/tape/drive/linux_tape.rs @@ -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 for TapeDensity { + type Error = Error; + + fn try_from(value: u8) -> Result { + 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 { + + 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 { + + 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 { + 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>, 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, 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 { + + 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 { + self.writer.write_all(data) + } + + fn bytes_written(&self) -> usize { + self.writer.bytes_written() + } + + fn finish(&mut self, incomplete: bool) -> Result { + 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() + } +} diff --git a/src/tape/drive/mod.rs b/src/tape/drive/mod.rs new file mode 100644 index 00000000..d1a633df --- /dev/null +++ b/src/tape/drive/mod.rs @@ -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; + + /// 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>, std::io::Error>; + + /// Write/Append a new file + fn write_file<'a>(&'a mut self) -> Result, 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 { + + 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; + + /// Read the media label + /// + /// This tries to read both media labels (label and media_set_label). + fn read_label(&mut self) -> Result, 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, 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, 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, + 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); + } + } +} diff --git a/src/tape/drive/virtual_tape.rs b/src/tape/drive/virtual_tape.rs new file mode 100644 index 00000000..34e29821 --- /dev/null +++ b/src/tape/drive/virtual_tape.rs @@ -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 { + 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, +} + +#[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 { + 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 { + 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 { + 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 { + 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>, 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, 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 { + + 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, 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, Error> { + let handle = self.open()?; + handle.list_media_changer_ids() + } +} diff --git a/src/tape/mod.rs b/src/tape/mod.rs index 85de1328..34798a23 100644 --- a/src/tape/mod.rs +++ b/src/tape/mod.rs @@ -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";