tape: add tape device driver
This commit is contained in:
		
							
								
								
									
										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"; | ||||
|  | ||||
|  | ||||
		Reference in New Issue
	
	Block a user