//! Driver for Linux SCSI tapes 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::sys::error::SysResult; use proxmox::tools::Uuid; use crate::{ config, backup::{ Fingerprint, KeyConfig, }, tools::run_command, api2::types::{ TapeDensity, MamAttribute, LinuxDriveAndMediaStatus, }, tape::{ TapeRead, TapeWrite, drive::{ linux_mtio::*, LinuxTapeDrive, TapeDriver, TapeAlertFlags, Lp17VolumeStatistics, read_mam_attributes, mam_extract_media_usage, read_tape_alert_flags, read_volume_statistics, set_encryption, }, file_formats::{ PROXMOX_TAPE_BLOCK_SIZE, PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, MediaSetLabel, MediaContentHeader, BlockedReader, BlockedWriter, }, }, }; fn run_sg_tape_cmd(subcmd: &str, args: &[&str], fd: RawFd) -> Result { 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) } /// Linux tape drive status #[derive(Debug)] pub struct LinuxDriveStatus { /// Size 0 is variable block size mode (default) pub blocksize: u32, /// Drive status flags pub status: GMTStatusFlags, /// Tape densitiy code (if drive media loaded) pub density: Option, /// Current file position if known (or -1) pub file_number: Option, /// Current block number if known (or -1) pub block_number: Option, } impl LinuxDriveStatus { pub fn tape_is_ready(&self) -> bool { self.status.contains(GMTStatusFlags::ONLINE) && !self.status.contains(GMTStatusFlags::DRIVE_OPEN) } } impl LinuxTapeDrive { /// 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 { proxmox::try_block!({ let file = open_linux_tape_device(&self.path)?; let mut handle = LinuxTapeHandle::new(file); let mut drive_status = handle.get_drive_status()?; if !drive_status.tape_is_ready() { // for autoloader only, try to reload ejected tapes if self.changer.is_some() { let _ = handle.mtload(); // just try, ignore error drive_status = handle.get_drive_status()?; } } if !drive_status.tape_is_ready() { bail!("tape not ready (no tape loaded)"); } if drive_status.blocksize == 0 { // device is variable block size - OK } 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 { // device is in fixed block size mode with correct block size } // Only root can set driver options, so we cannot // handle.set_default_options()?; Ok(handle) }).map_err(|err| format_err!("open drive '{}' ({}) failed - {}", self.name, self.path, err)) } } /// Linux Tape device handle pub struct LinuxTapeHandle { file: File, //_lock: File, } impl LinuxTapeHandle { /// Creates a new instance pub fn new(file: File) -> Self { Self { file } } /// 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::BUFFER_WRITES, true); // fixme: ?man st(4) claims we need to clear this for reliable multivolume opts.set(SetDrvBufferOptions::ASYNC_WRITES, true); opts.set(SetDrvBufferOptions::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(()) } /// call MTSETDRVBUFFER to set boolean options /// /// Note: this uses MT_ST_SETBOOLEANS pub fn drive_buffer_set_options(&self, opts: SetDrvBufferOptions) -> Result<(), Error> { let cmd = mtop { mt_op: MTCmd::MTSETDRVBUFFER, mt_count: (SetDrvBufferCmd::MT_ST_SETBOOLEANS as i32) | opts.bits(), }; unsafe { mtioctop(self.file.as_raw_fd(), &cmd) }.map_err(|err| format_err!("MTSETDRVBUFFER options failed - {}", err))?; Ok(()) } /// call MTSETDRVBUFFER to clear boolean options pub fn drive_buffer_clear_options(&self, opts: SetDrvBufferOptions) -> Result<(), Error> { let cmd = mtop { mt_op: MTCmd::MTSETDRVBUFFER, mt_count: (SetDrvBufferCmd::MT_ST_CLEARBOOLEANS 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(()) } pub fn mtop(&mut self, mt_op: MTCmd, mt_count: i32, msg: &str) -> Result<(), Error> { let cmd = mtop { mt_op, mt_count }; unsafe { mtioctop(self.file.as_raw_fd(), &cmd) }.map_err(|err| format_err!("{} failed (count {}) - {}", msg, mt_count, err))?; Ok(()) } pub fn mtload(&mut self) -> Result<(), Error> { let cmd = mtop { mt_op: MTCmd::MTLOAD, mt_count: 1, }; unsafe { mtioctop(self.file.as_raw_fd(), &cmd) }.map_err(|err| format_err!("MTLOAD failed - {}", err))?; Ok(()) } pub fn forward_space_count_files(&mut self, count: i32) -> Result<(), Error> { let cmd = mtop { mt_op: MTCmd::MTFSF, mt_count: count, }; unsafe { mtioctop(self.file.as_raw_fd(), &cmd) }.map_err(|err| { format_err!("forward space {} files failed - {}", count, err) })?; Ok(()) } pub fn backward_space_count_files(&mut self, count: i32) -> Result<(), Error> { let cmd = mtop { mt_op: MTCmd::MTBSF, mt_count: count, }; unsafe { mtioctop(self.file.as_raw_fd(), &cmd) }.map_err(|err| { format_err!("backward space {} files failed - {}", count, 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 and Media status pub fn get_drive_and_media_status(&mut self) -> Result { let drive_status = self.get_drive_status()?; let options = read_tapedev_options(&self.file)?; let alert_flags = self.tape_alert_flags() .map(|flags| format!("{:?}", flags)) .ok(); let mut status = LinuxDriveAndMediaStatus { blocksize: drive_status.blocksize, density: drive_status.density, status: format!("{:?}", drive_status.status), options: format!("{:?}", options), alert_flags, file_number: drive_status.file_number, block_number: drive_status.block_number, manufactured: None, bytes_read: None, bytes_written: None, medium_passes: None, medium_wearout: None, volume_mounts: None, }; if drive_status.tape_is_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) } /// Get Tape status/configuration with MTIOCGET ioctl pub fn get_drive_status(&mut self) -> Result { let _ = self.mtnop(); // ignore errors (i.e. no tape loaded) let mut status = mtget::default(); if let Err(err) = unsafe { mtiocget(self.file.as_raw_fd(), &mut status) } { bail!("MTIOCGET failed - {}", err); } 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; Ok(LinuxDriveStatus { blocksize, status: gmt, density: if density != 0 { Some(TapeDensity::try_from(density)?) } else { None }, file_number: if status.mt_fileno > 0 { Some(status.mt_fileno as u32) } else { None }, block_number: if status.mt_blkno > 0 { Some(status.mt_blkno as u32) } else { None }, }) } /// Read Cartridge Memory (MAM Attributes) /// /// Note: Only 'root' user may run RAW SG commands, so we need to /// spawn setuid binary 'sg-tape-cmd'. pub fn cartridge_memory(&mut self) -> Result, Error> { if nix::unistd::Uid::effective().is_root() { return read_mam_attributes(&mut self.file); } let output = run_sg_tape_cmd("cartridge-memory", &[], self.file.as_raw_fd())?; let result: Result, String> = serde_json::from_str(&output)?; result.map_err(|err| format_err!("{}", err)) } /// Read Volume Statistics /// /// Note: Only 'root' user may run RAW SG commands, so we need to /// spawn setuid binary 'sg-tape-cmd'. pub fn volume_statistics(&mut self) -> Result { if nix::unistd::Uid::effective().is_root() { return read_volume_statistics(&mut self.file); } let output = run_sg_tape_cmd("volume-statistics", &[], self.file.as_raw_fd())?; let result: Result = serde_json::from_str(&output)?; result.map_err(|err| format_err!("{}", err)) } } impl TapeDriver for LinuxTapeHandle { fn sync(&mut self) -> Result<(), Error> { println!("SYNC/FLUSH TAPE"); // MTWEOF with count 0 => flush let cmd = mtop { mt_op: MTCmd::MTWEOF, mt_count: 0 }; unsafe { mtioctop(self.file.as_raw_fd(), &cmd) }.map_err(|err| proxmox::io_format_err!("MT sync failed - {}", err))?; Ok(()) } /// Go to the end of the recorded media (for appending files). fn move_to_eom(&mut self) -> Result<(), Error> { let cmd = mtop { mt_op: MTCmd::MTEOM, mt_count: 1, }; unsafe { mtioctop(self.file.as_raw_fd(), &cmd) }.map_err(|err| format_err!("MTEOM failed - {}", err))?; Ok(()) } fn rewind(&mut self) -> Result<(), Error> { let cmd = mtop { mt_op: MTCmd::MTREW, mt_count: 1, }; unsafe { mtioctop(self.file.as_raw_fd(), &cmd) }.map_err(|err| format_err!("tape rewind failed - {}", err))?; Ok(()) } fn current_file_number(&mut self) -> Result { let mut status = mtget::default(); self.mtnop()?; if let Err(err) = unsafe { mtiocget(self.file.as_raw_fd(), &mut status) } { bail!("current_file_number MTIOCGET failed - {}", err); } if status.mt_fileno < 0 { bail!("current_file_number failed (got {})", status.mt_fileno); } Ok(status.mt_fileno as u64) } fn erase_media(&mut self, fast: bool) -> Result<(), Error> { self.rewind()?; // important - erase from BOT let cmd = mtop { mt_op: MTCmd::MTERASE, mt_count: if fast { 0 } else { 1 } }; unsafe { mtioctop(self.file.as_raw_fd(), &cmd) }.map_err(|err| format_err!("MTERASE failed - {}", err))?; Ok(()) } fn read_next_file<'a>(&'a mut self) -> Result>, std::io::Error> { match BlockedReader::open(&mut self.file)? { Some(reader) => Ok(Some(Box::new(reader))), None => Ok(None), } } fn write_file<'a>(&'a mut self) -> Result, std::io::Error> { let handle = TapeWriterHandle { writer: BlockedWriter::new(&mut self.file), }; Ok(Box::new(handle)) } fn write_media_set_label( &mut self, media_set_label: &MediaSetLabel, 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)?; let mut handle = TapeWriterHandle { writer: BlockedWriter::new(&mut self.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> { 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(()) } /// Read Tape Alert Flags /// /// Note: Only 'root' user may run RAW SG commands, so we need to /// spawn setuid binary 'sg-tape-cmd'. fn tape_alert_flags(&mut self) -> Result { if nix::unistd::Uid::effective().is_root() { return read_tape_alert_flags(&mut self.file); } let output = run_sg_tape_cmd("tape-alert-flags", &[], self.file.as_raw_fd())?; let result: Result = serde_json::from_str(&output)?; result .map_err(|err| format_err!("{}", err)) .map(TapeAlertFlags::from_bits_truncate) } /// Set or clear encryption key /// /// Note: Only 'root' user may run RAW SG commands, so we need to /// spawn setuid binary 'sg-tape-cmd'. Also, encryption key file /// is only readable by root. 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 set_encryption(&mut self.file, Some(tape_key)); } None => bail!("unknown tape encryption key '{}'", key_fingerprint), } } else { return set_encryption(&mut self.file, 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.file.as_raw_fd())? } else { run_sg_tape_cmd("encryption", &[], self.file.as_raw_fd())? }; let result: Result<(), String> = serde_json::from_str(&output)?; result.map_err(|err| format_err!("{}", err)) } } /// 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(()) } /// Check for correct Major/Minor numbers pub fn check_tape_is_linux_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 tape device"); } if (minor & 128) == 0 { bail!("Detected rewinding tape. Please use non-rewinding tape devices (/dev/nstX)."); } Ok(()) } /// Opens a Linux 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_linux_tape_device( path: &str, ) -> Result { 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_linux_tape_device(&file) .map_err(|err| format_err!("device type check {:?} failed - {}", path, err))?; Ok(file) } /// Read Linux tape device options from /sys pub fn read_tapedev_options(file: &File) -> Result { 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) }; let path = format!("/sys/dev/char/{}:{}/options", major, minor); let options = proxmox::tools::fs::file_read_firstline(&path)?; let options = options.trim(); let options = match options.strip_prefix("0x") { Some(rest) => rest, None => bail!("unable to parse '{}'", path), }; let options = i32::from_str_radix(&options, 16)?; Ok(SetDrvBufferOptions::from_bits_truncate(options)) } /// like BlockedWriter, but writes EOF mark on finish pub struct TapeWriterHandle<'a> { writer: BlockedWriter<&'a mut File>, } impl TapeWrite for TapeWriterHandle<'_> { fn write_all(&mut self, data: &[u8]) -> Result { self.writer.write_all(data) } fn bytes_written(&self) -> usize { self.writer.bytes_written() } fn finish(&mut self, incomplete: bool) -> Result { println!("FINISH TAPE HANDLE"); let leof = self.writer.finish(incomplete)?; tape_write_eof_mark(self.writer.writer_ref_mut())?; Ok(leof) } fn logical_end_of_media(&self) -> bool { self.writer.logical_end_of_media() } }