tape: implement LTO userspace driver
This commit is contained in:
		
							
								
								
									
										420
									
								
								src/tape/drive/lto/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										420
									
								
								src/tape/drive/lto/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,420 @@ | ||||
| //! Driver for LTO SCSI tapes | ||||
| //! | ||||
| //! This is a userspace drive implementation using SG_IO. | ||||
| //! | ||||
| //! Why we do not use the Linux tape driver: | ||||
| //! | ||||
| //! - missing features (MAM, Encryption, ...) | ||||
| //! | ||||
| //! - strange permission handling - only root (or CAP_SYS_RAWIO) can | ||||
| //!   do SG_IO (SYS_RAW_IO) | ||||
| //! | ||||
| //! - unability to detect EOT (you just get EIO) | ||||
|  | ||||
| mod sg_tape; | ||||
| pub use sg_tape::*; | ||||
|  | ||||
| use std::fs::{OpenOptions, File}; | ||||
| use std::os::unix::fs::OpenOptionsExt; | ||||
| use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; | ||||
| use std::convert::TryFrom; | ||||
|  | ||||
| use anyhow::{bail, format_err, Error}; | ||||
| use nix::fcntl::{fcntl, FcntlArg, OFlag}; | ||||
|  | ||||
| use proxmox::{ | ||||
|     tools::Uuid, | ||||
|     sys::error::SysResult, | ||||
| }; | ||||
|  | ||||
| use crate::{ | ||||
|     config, | ||||
|     tools::run_command, | ||||
|     backup::{ | ||||
|         Fingerprint, | ||||
|         KeyConfig, | ||||
|     }, | ||||
|     api2::types::{ | ||||
|         MamAttribute, | ||||
|         LtoDriveAndMediaStatus, | ||||
|         LtoTapeDrive, | ||||
|     }, | ||||
|     tape::{ | ||||
|         TapeRead, | ||||
|         TapeWrite, | ||||
|         drive::{ | ||||
|             TapeDriver, | ||||
|             TapeAlertFlags, | ||||
|             Lp17VolumeStatistics, | ||||
|             mam_extract_media_usage, | ||||
|         }, | ||||
|         file_formats::{ | ||||
|             PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, | ||||
|             MediaSetLabel, | ||||
|             MediaContentHeader, | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| impl LtoTapeDrive { | ||||
|  | ||||
|     /// Open a tape device | ||||
|     /// | ||||
|     /// This does additional checks: | ||||
|     /// | ||||
|     /// - check if it is a non-rewinding tape device | ||||
|     /// - check if drive is ready (tape loaded) | ||||
|     /// - check block size | ||||
|     /// - for autoloader only, try to reload ejected tapes | ||||
|     pub fn open(&self) -> Result<LtoTapeHandle, Error> { | ||||
|  | ||||
|         proxmox::try_block!({ | ||||
|             let file = open_lto_tape_device(&self.path)?; | ||||
|  | ||||
|             let mut handle = LtoTapeHandle::new(file)?; | ||||
|  | ||||
|             if !handle.sg_tape.test_unit_ready().is_ok() { | ||||
|                 // for autoloader only, try to reload ejected tapes | ||||
|                 if self.changer.is_some() { | ||||
|                     let _ = handle.sg_tape.load(); // just try, ignore error | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             handle.sg_tape.wait_until_ready()?; | ||||
|  | ||||
|             // Only root can set driver options, so we cannot | ||||
|             // handle.set_default_options()?; | ||||
|  | ||||
|             Ok(handle) | ||||
|         }).map_err(|err: Error| format_err!("open drive '{}' ({}) failed - {}", self.name, self.path, err)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Lto Tape device handle | ||||
| pub struct LtoTapeHandle { | ||||
|     sg_tape: SgTape, | ||||
| } | ||||
|  | ||||
| impl LtoTapeHandle { | ||||
|  | ||||
|     /// Creates a new instance | ||||
|     pub fn new(file: File) -> Result<Self, Error> { | ||||
|         let sg_tape = SgTape::new(file)?; | ||||
|         Ok(Self { sg_tape }) | ||||
|     } | ||||
|  | ||||
|     /// Set all options we need/want | ||||
|     pub fn set_default_options(&self) -> Result<(), Error> { | ||||
|         // fixme | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// Write a single EOF mark without flushing buffers | ||||
|     pub fn write_filemarks(&mut self, count: usize) -> Result<(), std::io::Error> { | ||||
|         self.sg_tape.write_filemarks(count, false) | ||||
|     } | ||||
|  | ||||
|     /// Get Tape and Media status | ||||
|     pub fn get_drive_and_media_status(&mut self) -> Result<LtoDriveAndMediaStatus, Error>  { | ||||
|  | ||||
|         let (file_number, block_number) = match self.sg_tape.position() { | ||||
|             Ok(position) => ( | ||||
|                 Some(position.logical_file_id), | ||||
|                 Some(position.logical_object_number), | ||||
|             ), | ||||
|             Err(_) => (None, None), | ||||
|         }; | ||||
|  | ||||
|         let options = String::from("FIXME"); | ||||
|  | ||||
|         let alert_flags = self.tape_alert_flags() | ||||
|             .map(|flags| format!("{:?}", flags)) | ||||
|             .ok(); | ||||
|  | ||||
|         let mut status = LtoDriveAndMediaStatus { | ||||
|             blocksize: 0, // fixme: remove | ||||
|             density: None, // fixme | ||||
|             status: String::from("FIXME"), | ||||
|             options, | ||||
|             alert_flags, | ||||
|             file_number, | ||||
|             block_number, | ||||
|             manufactured: None, | ||||
|             bytes_read: None, | ||||
|             bytes_written: None, | ||||
|             medium_passes: None, | ||||
|             medium_wearout: None, | ||||
|             volume_mounts: None, | ||||
|         }; | ||||
|  | ||||
|         if self.sg_tape.test_unit_ready()? { | ||||
|  | ||||
|             if let Ok(mam) = self.cartridge_memory() { | ||||
|  | ||||
|                 let usage = mam_extract_media_usage(&mam)?; | ||||
|  | ||||
|                 status.manufactured = Some(usage.manufactured); | ||||
|                 status.bytes_read = Some(usage.bytes_read); | ||||
|                 status.bytes_written = Some(usage.bytes_written); | ||||
|  | ||||
|                 if let Ok(volume_stats) = self.volume_statistics() { | ||||
|  | ||||
|                     let passes = std::cmp::max( | ||||
|                         volume_stats.beginning_of_medium_passes, | ||||
|                         volume_stats.middle_of_tape_passes, | ||||
|                     ); | ||||
|  | ||||
|                     // assume max. 16000 medium passes | ||||
|                     // see: https://en.wikipedia.org/wiki/Linear_Tape-Open | ||||
|                     let wearout: f64 = (passes as f64)/(16000.0 as f64); | ||||
|  | ||||
|                     status.medium_passes = Some(passes); | ||||
|                     status.medium_wearout = Some(wearout); | ||||
|  | ||||
|                     status.volume_mounts = Some(volume_stats.volume_mounts); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Ok(status) | ||||
|     } | ||||
|  | ||||
|     pub fn load(&mut self) ->  Result<(), Error> { | ||||
|         self.sg_tape.load() | ||||
|     } | ||||
|  | ||||
|     /// Read Cartridge Memory (MAM Attributes) | ||||
|     pub fn cartridge_memory(&mut self) -> Result<Vec<MamAttribute>, Error> { | ||||
|         self.sg_tape.cartridge_memory() | ||||
|      } | ||||
|  | ||||
|     /// Read Volume Statistics | ||||
|     pub fn volume_statistics(&mut self) -> Result<Lp17VolumeStatistics, Error> { | ||||
|         self.sg_tape.volume_statistics() | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| impl TapeDriver for LtoTapeHandle { | ||||
|  | ||||
|     fn sync(&mut self) -> Result<(), Error> { | ||||
|         self.sg_tape.sync()?; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// Go to the end of the recorded media (for appending files). | ||||
|     fn move_to_eom(&mut self) -> Result<(), Error> { | ||||
|         self.sg_tape.move_to_eom() | ||||
|     } | ||||
|  | ||||
|     fn forward_space_count_files(&mut self, count: usize) -> Result<(), Error> { | ||||
|         self.sg_tape.space_filemarks(isize::try_from(count)?) | ||||
|     } | ||||
|  | ||||
|     fn backward_space_count_files(&mut self, count: usize) -> Result<(), Error> { | ||||
|         self.sg_tape.space_filemarks(-isize::try_from(count)?) | ||||
|     } | ||||
|  | ||||
|     fn rewind(&mut self) -> Result<(), Error> { | ||||
|         self.sg_tape.rewind() | ||||
|     } | ||||
|  | ||||
|     fn current_file_number(&mut self) -> Result<u64, Error> { | ||||
|         self.sg_tape.current_file_number() | ||||
|     } | ||||
|  | ||||
|     fn erase_media(&mut self, fast: bool) -> Result<(), Error> { | ||||
|         self.rewind()?; // important - erase from BOT | ||||
|         self.sg_tape.erase_media(fast) | ||||
|     } | ||||
|  | ||||
|     fn read_next_file<'a>(&'a mut self) -> Result<Option<Box<dyn TapeRead + 'a>>, std::io::Error> { | ||||
|         let reader = self.sg_tape.open_reader()?; | ||||
|         let handle = match reader { | ||||
|             Some(reader) => { | ||||
|                 let reader: Box<dyn TapeRead> = Box::new(reader); | ||||
|                 Some(reader) | ||||
|             } | ||||
|             None => None, | ||||
|         }; | ||||
|  | ||||
|         Ok(handle) | ||||
|     } | ||||
|  | ||||
|     fn write_file<'a>(&'a mut self) -> Result<Box<dyn TapeWrite + 'a>, std::io::Error> { | ||||
|         let handle = self.sg_tape.open_writer(); | ||||
|         Ok(Box::new(handle)) | ||||
|     } | ||||
|  | ||||
|     fn write_media_set_label( | ||||
|         &mut self, | ||||
|         media_set_label: &MediaSetLabel, | ||||
|         key_config: Option<&KeyConfig>, | ||||
|     ) -> Result<(), Error> { | ||||
|  | ||||
|         let file_number = self.current_file_number()?; | ||||
|         if file_number != 1 { | ||||
|             self.rewind()?; | ||||
|             self.forward_space_count_files(1)?; // skip label | ||||
|         } | ||||
|  | ||||
|         let file_number = self.current_file_number()?; | ||||
|         if file_number != 1 { | ||||
|             bail!("write_media_set_label failed - got wrong file number ({} != 1)", file_number); | ||||
|         } | ||||
|  | ||||
|         self.set_encryption(None)?; | ||||
|  | ||||
|         { // limit handle scope | ||||
|             let mut handle = self.write_file()?; | ||||
|  | ||||
|             let mut value = serde_json::to_value(media_set_label)?; | ||||
|             if media_set_label.encryption_key_fingerprint.is_some() { | ||||
|                 match key_config { | ||||
|                     Some(key_config) => { | ||||
|                         value["key-config"] = serde_json::to_value(key_config)?; | ||||
|                     } | ||||
|                     None => { | ||||
|                         bail!("missing encryption key config"); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             let raw = serde_json::to_string_pretty(&value)?; | ||||
|  | ||||
|             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(()) | ||||
|     } | ||||
|  | ||||
|     /// Rewind and put the drive off line (Eject media). | ||||
|     fn eject_media(&mut self) -> Result<(), Error> { | ||||
|         self.sg_tape.eject() | ||||
|     } | ||||
|  | ||||
|     /// Read Tape Alert Flags | ||||
|     fn tape_alert_flags(&mut self) -> Result<TapeAlertFlags, Error> { | ||||
|         self.sg_tape.tape_alert_flags() | ||||
|     } | ||||
|  | ||||
|     /// Set or clear encryption key | ||||
|     /// | ||||
|     /// Note: Only 'root' can read secret encryption keys, so we need | ||||
|     /// to spawn setuid binary 'sg-tape-cmd'. | ||||
|     fn set_encryption( | ||||
|         &mut self, | ||||
|         key_fingerprint: Option<(Fingerprint, Uuid)>, | ||||
|     ) -> Result<(), Error> { | ||||
|  | ||||
|         if nix::unistd::Uid::effective().is_root() { | ||||
|  | ||||
|             if let Some((ref key_fingerprint, ref uuid)) = key_fingerprint { | ||||
|  | ||||
|                 let (key_map, _digest) = config::tape_encryption_keys::load_keys()?; | ||||
|                 match key_map.get(key_fingerprint) { | ||||
|                     Some(item) => { | ||||
|  | ||||
|                         // derive specialized key for each media-set | ||||
|  | ||||
|                         let mut tape_key = [0u8; 32]; | ||||
|  | ||||
|                         let uuid_bytes: [u8; 16] = uuid.as_bytes().clone(); | ||||
|  | ||||
|                         openssl::pkcs5::pbkdf2_hmac( | ||||
|                             &item.key, | ||||
|                             &uuid_bytes, | ||||
|                             10, | ||||
|                             openssl::hash::MessageDigest::sha256(), | ||||
|                             &mut tape_key)?; | ||||
|  | ||||
|                         return self.sg_tape.set_encryption(Some(tape_key)); | ||||
|                     } | ||||
|                     None => bail!("unknown tape encryption key '{}'", key_fingerprint), | ||||
|                 } | ||||
|             } else { | ||||
|                 return self.sg_tape.set_encryption(None); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         let output = if let Some((fingerprint, uuid)) = key_fingerprint { | ||||
|             let fingerprint = crate::tools::format::as_fingerprint(fingerprint.bytes()); | ||||
|             run_sg_tape_cmd("encryption", &[ | ||||
|                 "--fingerprint", &fingerprint, | ||||
|                 "--uuid", &uuid.to_string(), | ||||
|             ], self.sg_tape.file_mut().as_raw_fd())? | ||||
|         } else { | ||||
|             run_sg_tape_cmd("encryption", &[], self.sg_tape.file_mut().as_raw_fd())? | ||||
|         }; | ||||
|         let result: Result<(), String> = serde_json::from_str(&output)?; | ||||
|         result.map_err(|err| format_err!("{}", err)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Check for correct Major/Minor numbers | ||||
| pub fn check_tape_is_lto_tape_device(file: &File) -> Result<(), Error> { | ||||
|  | ||||
|     let stat = nix::sys::stat::fstat(file.as_raw_fd())?; | ||||
|  | ||||
|     let devnum = stat.st_rdev; | ||||
|  | ||||
|     let major = unsafe { libc::major(devnum) }; | ||||
|     let _minor = unsafe { libc::minor(devnum) }; | ||||
|  | ||||
|     if major == 9 { | ||||
|         bail!("not a scsi-generic tape device (cannot use linux tape devices)"); | ||||
|     } | ||||
|  | ||||
|     if major != 21 { | ||||
|         bail!("not a scsi-generic tape device"); | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Opens a Lto tape device | ||||
| /// | ||||
| /// The open call use O_NONBLOCK, but that flag is cleard after open | ||||
| /// succeeded. This also checks if the device is a non-rewinding tape | ||||
| /// device. | ||||
| pub fn open_lto_tape_device( | ||||
|     path: &str, | ||||
| ) -> Result<File, Error> { | ||||
|  | ||||
|     let file = OpenOptions::new() | ||||
|         .read(true) | ||||
|         .write(true) | ||||
|         .custom_flags(libc::O_NONBLOCK) | ||||
|         .open(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()?; | ||||
|  | ||||
|     check_tape_is_lto_tape_device(&file) | ||||
|         .map_err(|err| format_err!("device type check {:?} failed - {}", path, err))?; | ||||
|  | ||||
|     Ok(file) | ||||
| } | ||||
|  | ||||
| fn run_sg_tape_cmd(subcmd: &str, args: &[&str], fd: RawFd) -> Result<String, Error> { | ||||
|     let mut command = std::process::Command::new( | ||||
|         "/usr/lib/x86_64-linux-gnu/proxmox-backup/sg-tape-cmd"); | ||||
|     command.args(&[subcmd]); | ||||
|     command.args(&["--stdin"]); | ||||
|     command.args(args); | ||||
|     let device_fd = nix::unistd::dup(fd)?; | ||||
|     command.stdin(unsafe { std::process::Stdio::from_raw_fd(device_fd)}); | ||||
|     run_command(command, None) | ||||
| } | ||||
							
								
								
									
										445
									
								
								src/tape/drive/lto/sg_tape.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										445
									
								
								src/tape/drive/lto/sg_tape.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,445 @@ | ||||
| use std::time::SystemTime; | ||||
| use std::fs::{File, OpenOptions}; | ||||
| use std::os::unix::fs::OpenOptionsExt; | ||||
| use std::os::unix::io::AsRawFd; | ||||
| use std::path::Path; | ||||
|  | ||||
| use anyhow::{bail, format_err, Error}; | ||||
| use endian_trait::Endian; | ||||
| use nix::fcntl::{fcntl, FcntlArg, OFlag}; | ||||
|  | ||||
| use proxmox::{ | ||||
|     sys::error::SysResult, | ||||
|     tools::io::ReadExt, | ||||
| }; | ||||
|  | ||||
| use crate::{ | ||||
|     api2::types::{ | ||||
|         MamAttribute, | ||||
|     }, | ||||
|     tape::{ | ||||
|         BlockRead, | ||||
|         BlockReadStatus, | ||||
|         BlockWrite, | ||||
|         file_formats::{ | ||||
|             BlockedWriter, | ||||
|             BlockedReader, | ||||
|         }, | ||||
|         drive::{ | ||||
|             TapeAlertFlags, | ||||
|             Lp17VolumeStatistics, | ||||
|             read_mam_attributes, | ||||
|             read_tape_alert_flags, | ||||
|             read_volume_statistics, | ||||
|             set_encryption, | ||||
|         }, | ||||
|     }, | ||||
|     tools::sgutils2::{ | ||||
|         SgRaw, | ||||
|         SenseInfo, | ||||
|         ScsiError, | ||||
|         InquiryInfo, | ||||
|         scsi_inquiry, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| #[repr(C, packed)] | ||||
| #[derive(Endian, Debug, Copy, Clone)] | ||||
| pub struct ReadPositionLongPage { | ||||
|     flags: u8, | ||||
|     reserved: [u8;3], | ||||
|     partition_number: u32, | ||||
|     pub logical_object_number: u64, | ||||
|     pub logical_file_id: u64, | ||||
|     obsolete: [u8;8], | ||||
| } | ||||
|  | ||||
| pub struct SgTape { | ||||
|     file: File, | ||||
| } | ||||
|  | ||||
| impl SgTape { | ||||
|  | ||||
|     const SCSI_TAPE_DEFAULT_TIMEOUT: usize = 60*2; // 2 minutes | ||||
|  | ||||
|     /// Create a new instance | ||||
|     /// | ||||
|     /// Uses scsi_inquiry to check the device type. | ||||
|     pub fn new(mut file: File) -> Result<Self, Error> { | ||||
|  | ||||
|         let info = scsi_inquiry(&mut file)?; | ||||
|  | ||||
|         if info.peripheral_type != 1 { | ||||
|             bail!("not a tape device (peripheral_type = {})", info.peripheral_type); | ||||
|         } | ||||
|         Ok(Self { file }) | ||||
|     } | ||||
|  | ||||
|     pub fn open<P: AsRef<Path>>(path: P) -> Result<SgTape, Error> { | ||||
|         // do not wait for media, use O_NONBLOCK | ||||
|         let file = OpenOptions::new() | ||||
|             .read(true) | ||||
|             .write(true) | ||||
|             .custom_flags(libc::O_NONBLOCK) | ||||
|             .open(path)?; | ||||
|  | ||||
|         // then clear O_NONBLOCK | ||||
|         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()?; | ||||
|  | ||||
|         Self::new(file) | ||||
|     } | ||||
|  | ||||
|     pub fn inquiry(&mut self) -> Result<InquiryInfo, Error> { | ||||
|         scsi_inquiry(&mut self.file) | ||||
|     } | ||||
|  | ||||
|     pub fn erase_media(&mut self, _fast: bool) -> Result<(), Error> { | ||||
|         // fixme: | ||||
|         unimplemented!(); | ||||
|     } | ||||
|  | ||||
|     pub fn rewind(&mut self) -> Result<(), Error> { | ||||
|  | ||||
|         let mut sg_raw = SgRaw::new(&mut self.file, 16)?; | ||||
|         sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT); | ||||
|         let mut cmd = Vec::new(); | ||||
|         cmd.extend(&[0x01, 0, 0, 0, 0, 0]); // REWIND | ||||
|  | ||||
|         sg_raw.do_command(&cmd) | ||||
|             .map_err(|err| format_err!("rewind failed - {}", err))?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub fn position(&mut self) -> Result<ReadPositionLongPage, Error> { | ||||
|  | ||||
|         let expected_size = std::mem::size_of::<ReadPositionLongPage>(); | ||||
|  | ||||
|         let mut sg_raw = SgRaw::new(&mut self.file, 32)?; | ||||
|         sg_raw.set_timeout(30); // use short timeout | ||||
|         let mut cmd = Vec::new(); | ||||
|         cmd.extend(&[0x34, 0x06, 0, 0, 0, 0, 0, 0, 0, 0]); // READ POSITION LONG FORM | ||||
|  | ||||
|         let data = sg_raw.do_command(&cmd) | ||||
|             .map_err(|err| format_err!("read position failed - {}", err))?; | ||||
|  | ||||
|         let page = proxmox::try_block!({ | ||||
|             if data.len() != expected_size { | ||||
|                 bail!("got unexpected data len ({} != {}", data.len(), expected_size); | ||||
|             } | ||||
|  | ||||
|             let mut reader = &data[..]; | ||||
|  | ||||
|             let page: ReadPositionLongPage = unsafe { reader.read_be_value()? }; | ||||
|  | ||||
|             Ok(page) | ||||
|         }).map_err(|err: Error| format_err!("decode position page failed - {}", err))?; | ||||
|  | ||||
|         if page.partition_number != 0 { | ||||
|             bail!("detecthed partitioned tape - not supported"); | ||||
|         } | ||||
|  | ||||
|         println!("DATA: {:?}", page); | ||||
|  | ||||
|         Ok(page) | ||||
|     } | ||||
|  | ||||
|     pub fn current_file_number(&mut self) -> Result<u64, Error> { | ||||
|         let position = self.position()?; | ||||
|         Ok(position.logical_file_id) | ||||
|     } | ||||
|  | ||||
|     pub fn locate(&mut self) ->  Result<(), Error> { | ||||
|         // fixme: impl LOCATE | ||||
|         unimplemented!(); | ||||
|     } | ||||
|  | ||||
|     pub fn move_to_eom(&mut self) ->  Result<(), Error> { | ||||
|         let mut sg_raw = SgRaw::new(&mut self.file, 16)?; | ||||
|         sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT); | ||||
|         let mut cmd = Vec::new(); | ||||
|         cmd.extend(&[0x11, 0x03, 0, 0, 0, 0]); // SPACE(6) move to EOD | ||||
|  | ||||
|         sg_raw.do_command(&cmd) | ||||
|             .map_err(|err| format_err!("move to EOD failed - {}", err))?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub fn space_filemarks(&mut self, count: isize) ->  Result<(), Error> { | ||||
|         let mut sg_raw = SgRaw::new(&mut self.file, 16)?; | ||||
|         sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT); | ||||
|         let mut cmd = Vec::new(); | ||||
|  | ||||
|         // Use short command if possible (supported by all drives) | ||||
|         if (count <= 0x7fffff) && (count > -0x7fffff) { | ||||
|             cmd.extend(&[0x11, 0x01]); // SPACE(6) with filemarks | ||||
|             cmd.push(((count >> 16) & 0xff) as u8); | ||||
|             cmd.push(((count >> 8) & 0xff) as u8); | ||||
|             cmd.push((count & 0xff) as u8); | ||||
|             cmd.push(0); //control byte | ||||
|         } else { | ||||
|  | ||||
|             cmd.extend(&[0x91, 0x01, 0, 0]); // SPACE(16) with filemarks | ||||
|             let count: i64 = count as i64; | ||||
|             cmd.extend(&count.to_be_bytes()); | ||||
|             cmd.extend(&[0, 0, 0, 0]); | ||||
|         } | ||||
|  | ||||
|         sg_raw.do_command(&cmd) | ||||
|             .map_err(|err| format_err!("space filemarks failed - {}", err))?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub fn eject(&mut self) ->  Result<(), Error> { | ||||
|         let mut sg_raw = SgRaw::new(&mut self.file, 16)?; | ||||
|         sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT); | ||||
|         let mut cmd = Vec::new(); | ||||
|         cmd.extend(&[0x1B, 0, 0, 0, 0, 0]); // LODA/UNLOAD HOLD=0, LOAD=0 | ||||
|  | ||||
|         sg_raw.do_command(&cmd) | ||||
|             .map_err(|err| format_err!("eject failed - {}", err))?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub fn load(&mut self) ->  Result<(), Error> { | ||||
|         let mut sg_raw = SgRaw::new(&mut self.file, 16)?; | ||||
|         sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT); | ||||
|         let mut cmd = Vec::new(); | ||||
|         cmd.extend(&[0x1B, 0, 0, 0, 0b0000_0001, 0]); // LODA/UNLOAD HOLD=0, LOAD=1 | ||||
|  | ||||
|         sg_raw.do_command(&cmd) | ||||
|             .map_err(|err| format_err!("load media failed - {}", err))?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub fn write_filemarks( | ||||
|         &mut self, | ||||
|         count: usize, | ||||
|         immediate: bool, | ||||
|     ) ->  Result<(), std::io::Error> { | ||||
|  | ||||
|         if count > 255 { | ||||
|             proxmox::io_bail!("write_filemarks failed: got strange count '{}'", count); | ||||
|         } | ||||
|  | ||||
|         let mut sg_raw = SgRaw::new(&mut self.file, 16) | ||||
|             .map_err(|err| proxmox::io_format_err!("write_filemarks failed (alloc) - {}", err))?; | ||||
|  | ||||
|         sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT); | ||||
|         let mut cmd = Vec::new(); | ||||
|         cmd.push(0x10); | ||||
|         if immediate { | ||||
|             cmd.push(1); // IMMED=1 | ||||
|         } else { | ||||
|             cmd.push(0); // IMMED=0 | ||||
|         } | ||||
|         cmd.extend(&[0, 0, count as u8]); // COUNT | ||||
|         cmd.push(0); // control byte | ||||
|  | ||||
|         sg_raw.do_command(&cmd) | ||||
|             .map_err(|err| proxmox::io_format_err!("write filemark failed - {}", err))?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     // Flush tape buffers (WEOF with count 0 => flush) | ||||
|     pub fn sync(&mut self) -> Result<(), std::io::Error> { | ||||
|         self.write_filemarks(0, false)?; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub fn test_unit_ready(&mut self) -> Result<bool, Error> { | ||||
|  | ||||
|         let mut sg_raw = SgRaw::new(&mut self.file, 16)?; | ||||
|         sg_raw.set_timeout(30); // use short timeout | ||||
|         let mut cmd = Vec::new(); | ||||
|         cmd.extend(&[0x00, 0, 0, 0, 0, 0]); // TEST UNIT READY | ||||
|  | ||||
|         // fixme: check sense | ||||
|         sg_raw.do_command(&cmd) | ||||
|             .map_err(|err| format_err!("unit not ready - {}", err))?; | ||||
|  | ||||
|         Ok(true) | ||||
|  | ||||
|     } | ||||
|  | ||||
|     pub fn wait_until_ready(&mut self) -> Result<(), Error> { | ||||
|  | ||||
|         let start = SystemTime::now(); | ||||
|         let max_wait = std::time::Duration::new(Self::SCSI_TAPE_DEFAULT_TIMEOUT as u64, 0); | ||||
|  | ||||
|         loop { | ||||
|             match self.test_unit_ready() { | ||||
|                 Ok(true) => return Ok(()), | ||||
|                 _ => { | ||||
|                     std::thread::sleep(std::time::Duration::new(1, 0)); | ||||
|                     if start.elapsed()? > max_wait { | ||||
|                         bail!("wait_until_ready failed - got timeout"); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Read Tape Alert Flags | ||||
|     pub fn tape_alert_flags(&mut self) -> Result<TapeAlertFlags, Error> { | ||||
|         read_tape_alert_flags(&mut self.file) | ||||
|     } | ||||
|  | ||||
|     /// Read Cartridge Memory (MAM Attributes) | ||||
|     pub fn cartridge_memory(&mut self) -> Result<Vec<MamAttribute>, Error> { | ||||
|         read_mam_attributes(&mut self.file) | ||||
|     } | ||||
|  | ||||
|     /// Read Volume Statistics | ||||
|     pub fn volume_statistics(&mut self) -> Result<Lp17VolumeStatistics, Error> { | ||||
|         return read_volume_statistics(&mut self.file); | ||||
|     } | ||||
|  | ||||
|     pub fn set_encryption( | ||||
|         &mut self, | ||||
|         key: Option<[u8; 32]>, | ||||
|     ) -> Result<(), Error> { | ||||
|         set_encryption(&mut self.file, key) | ||||
|     } | ||||
|  | ||||
|     // Note: use alloc_page_aligned_buffer to alloc data transfer buffer | ||||
|     // | ||||
|     // Returns true if the drive reached the Logical End Of Media (early warning) | ||||
|     fn write_block(&mut self, data: &[u8]) -> Result<bool, std::io::Error> { | ||||
|  | ||||
|         let transfer_len = data.len(); | ||||
|  | ||||
|         if transfer_len > 0xFFFFFF { | ||||
|            proxmox::io_bail!("write failed - data too large"); | ||||
|         } | ||||
|  | ||||
|         let mut sg_raw = SgRaw::new(&mut self.file, 0) | ||||
|             .unwrap(); // cannot fail with size 0 | ||||
|  | ||||
|         sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT); | ||||
|         let mut cmd = Vec::new(); | ||||
|         cmd.push(0x0A);  // WRITE | ||||
|         cmd.push(0x00); // VARIABLE SIZED BLOCKS | ||||
|         cmd.push(((transfer_len >> 16) & 0xff) as u8); | ||||
|         cmd.push(((transfer_len >> 8) & 0xff) as u8); | ||||
|         cmd.push((transfer_len & 0xff) as u8); | ||||
|         cmd.push(0); // control byte | ||||
|  | ||||
|         //println!("WRITE {:?}", cmd); | ||||
|         //println!("WRITE {:?}", data); | ||||
|  | ||||
|         sg_raw.do_out_command(&cmd, data) | ||||
|             .map_err(|err| proxmox::io_format_err!("write failed - {}", err))?; | ||||
|  | ||||
|         // fixme: LEOM? | ||||
|  | ||||
|         Ok(false) | ||||
|     } | ||||
|  | ||||
|     fn read_block(&mut self, buffer: &mut [u8]) -> Result<BlockReadStatus, std::io::Error> { | ||||
|         let transfer_len = buffer.len(); | ||||
|  | ||||
|         if transfer_len > 0xFFFFFF { | ||||
|             proxmox::io_bail!("read failed - buffer too large"); | ||||
|         } | ||||
|  | ||||
|         let mut sg_raw = SgRaw::new(&mut self.file, 0) | ||||
|             .unwrap(); // cannot fail with size 0 | ||||
|  | ||||
|         sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT); | ||||
|         let mut cmd = Vec::new(); | ||||
|         cmd.push(0x08); // READ | ||||
|         cmd.push(0x02); // VARIABLE SIZED BLOCKS, SILI=1 | ||||
|         //cmd.push(0x00); // VARIABLE SIZED BLOCKS, SILI=0 | ||||
|         cmd.push(((transfer_len >> 16) & 0xff) as u8); | ||||
|         cmd.push(((transfer_len >> 8) & 0xff) as u8); | ||||
|         cmd.push((transfer_len & 0xff) as u8); | ||||
|         cmd.push(0); // control byte | ||||
|  | ||||
|         let data = match sg_raw.do_in_command(&cmd, buffer) { | ||||
|             Ok(data) => data, | ||||
|             Err(ScsiError::Sense(SenseInfo { sense_key: 0, asc: 0, ascq: 1 })) => { | ||||
|                 return Ok(BlockReadStatus::EndOfFile); | ||||
|             } | ||||
|             Err(ScsiError::Sense(SenseInfo { sense_key: 8, asc: 0, ascq: 5 })) => { | ||||
|                 return Ok(BlockReadStatus::EndOfStream); | ||||
|             } | ||||
|             Err(err) => { | ||||
|                 println!("READ ERR {:?}", err); | ||||
|                 proxmox::io_bail!("read failed - {}", err); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         if data.len() != transfer_len { | ||||
|             proxmox::io_bail!("read failed - unexpected block len ({} != {})", data.len(), buffer.len()) | ||||
|         } | ||||
|  | ||||
|         Ok(BlockReadStatus::Ok(transfer_len)) | ||||
|     } | ||||
|  | ||||
|     pub fn open_writer(&mut self) -> BlockedWriter<SgTapeWriter> { | ||||
|         let writer = SgTapeWriter::new(self); | ||||
|         BlockedWriter::new(writer) | ||||
|     } | ||||
|  | ||||
|     pub fn open_reader(&mut self) -> Result<Option<BlockedReader<SgTapeReader>>, std::io::Error> { | ||||
|         let reader = SgTapeReader::new(self); | ||||
|         match BlockedReader::open(reader)? { | ||||
|             Some(reader) => Ok(Some(reader)), | ||||
|             None => Ok(None), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct SgTapeReader<'a> { | ||||
|     sg_tape: &'a mut SgTape, | ||||
| } | ||||
|  | ||||
| impl <'a> SgTapeReader<'a> { | ||||
|  | ||||
|     pub fn new(sg_tape: &'a mut SgTape) -> Self { | ||||
|         Self { sg_tape } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl <'a> BlockRead for SgTapeReader<'a> { | ||||
|  | ||||
|     fn read_block(&mut self, buffer: &mut [u8]) -> Result<BlockReadStatus, std::io::Error> { | ||||
|         self.sg_tape.read_block(buffer) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct SgTapeWriter<'a> { | ||||
|     sg_tape: &'a mut SgTape, | ||||
|     _leom_sent: bool, | ||||
| } | ||||
|  | ||||
| impl <'a> SgTapeWriter<'a> { | ||||
|  | ||||
|     pub fn new(sg_tape: &'a mut SgTape) -> Self { | ||||
|         Self { sg_tape, _leom_sent: false } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl <'a> BlockWrite for SgTapeWriter<'a> { | ||||
|  | ||||
|     fn write_block(&mut self, buffer: &[u8]) -> Result<bool, std::io::Error> { | ||||
|         self.sg_tape.write_block(buffer) | ||||
|     } | ||||
|  | ||||
|     fn write_filemark(&mut self) -> Result<(), std::io::Error> { | ||||
|         self.sg_tape.write_filemarks(1, true) | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user