1 //! Driver for LTO SCSI tapes
3 //! This is a userspace drive implementation using SG_IO.
5 //! Why we do not use the Linux tape driver:
7 //! - missing features (MAM, Encryption, ...)
9 //! - strange permission handling - only root (or CAP_SYS_RAWIO) can
10 //! do SG_IO (SYS_RAW_IO)
12 //! - unability to detect EOT (you just get EIO)
17 use std
::fs
::{OpenOptions, File}
;
18 use std
::os
::unix
::fs
::OpenOptionsExt
;
19 use std
::os
::unix
::io
::{AsRawFd, FromRawFd, RawFd}
;
20 use std
::convert
::TryInto
;
22 use anyhow
::{bail, format_err, Error}
;
23 use nix
::fcntl
::{fcntl, FcntlArg, OFlag}
;
27 sys
::error
::SysResult
,
39 LtoDriveAndMediaStatus
,
49 mam_extract_media_usage
,
52 PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0
,
61 /// Open a tape device
63 /// This does additional checks:
65 /// - check if it is a non-rewinding tape device
66 /// - check if drive is ready (tape loaded)
67 /// - check block size
68 /// - for autoloader only, try to reload ejected tapes
69 pub fn open(&self) -> Result
<LtoTapeHandle
, Error
> {
72 let file
= open_lto_tape_device(&self.path
)?
;
74 let mut handle
= LtoTapeHandle
::new(file
)?
;
76 if !handle
.sg_tape
.test_unit_ready().is_ok() {
77 // for autoloader only, try to reload ejected tapes
78 if self.changer
.is_some() {
79 let _
= handle
.sg_tape
.load(); // just try, ignore error
83 handle
.sg_tape
.wait_until_ready()?
;
85 handle
.set_default_options()?
;
88 }).map_err(|err
: Error
| format_err
!("open drive '{}' ({}) failed - {}", self.name
, self.path
, err
))
92 /// Lto Tape device handle
93 pub struct LtoTapeHandle
{
99 /// Creates a new instance
100 pub fn new(file
: File
) -> Result
<Self, Error
> {
101 let sg_tape
= SgTape
::new(file
)?
;
105 /// Set all options we need/want
106 pub fn set_default_options(&mut self) -> Result
<(), Error
> {
108 let compression
= Some(true);
109 let block_length
= Some(0); // variable length mode
110 let buffer_mode
= Some(true); // Always use drive buffer
112 self.set_drive_options(compression
, block_length
, buffer_mode
)?
;
117 /// Set driver options
118 pub fn set_drive_options(
120 compression
: Option
<bool
>,
121 block_length
: Option
<u32>,
122 buffer_mode
: Option
<bool
>,
123 ) -> Result
<(), Error
> {
124 self.sg_tape
.set_drive_options(compression
, block_length
, buffer_mode
)
127 /// Write a single EOF mark without flushing buffers
128 pub fn write_filemarks(&mut self, count
: usize) -> Result
<(), std
::io
::Error
> {
129 self.sg_tape
.write_filemarks(count
, false)
132 /// Get Tape and Media status
133 pub fn get_drive_and_media_status(&mut self) -> Result
<LtoDriveAndMediaStatus
, Error
> {
135 let drive_status
= self.sg_tape
.read_drive_status()?
;
137 let alert_flags
= self.tape_alert_flags()
138 .map(|flags
| format
!("{:?}", flags
))
141 let mut status
= LtoDriveAndMediaStatus
{
142 vendor
: self.sg_tape
.info().vendor
.clone(),
143 product
: self.sg_tape
.info().product
.clone(),
144 revision
: self.sg_tape
.info().revision
.clone(),
145 blocksize
: drive_status
.block_length
,
146 compression
: drive_status
.compression
,
147 buffer_mode
: drive_status
.buffer_mode
,
148 density
: drive_status
.density_code
.try_into()?
,
157 medium_wearout
: None
,
161 if self.sg_tape
.test_unit_ready().is_ok() {
163 if drive_status
.write_protect
{
164 status
.write_protect
= Some(drive_status
.write_protect
);
167 let position
= self.sg_tape
.position()?
;
169 status
.file_number
= Some(position
.logical_file_id
);
170 status
.block_number
= Some(position
.logical_object_number
);
172 if let Ok(mam
) = self.cartridge_memory() {
174 let usage
= mam_extract_media_usage(&mam
)?
;
176 status
.manufactured
= Some(usage
.manufactured
);
177 status
.bytes_read
= Some(usage
.bytes_read
);
178 status
.bytes_written
= Some(usage
.bytes_written
);
180 if let Ok(volume_stats
) = self.volume_statistics() {
182 let passes
= std
::cmp
::max(
183 volume_stats
.beginning_of_medium_passes
,
184 volume_stats
.middle_of_tape_passes
,
187 // assume max. 16000 medium passes
188 // see: https://en.wikipedia.org/wiki/Linear_Tape-Open
189 let wearout
: f64 = (passes
as f64)/(16000.0 as f64);
191 status
.medium_passes
= Some(passes
);
192 status
.medium_wearout
= Some(wearout
);
194 status
.volume_mounts
= Some(volume_stats
.volume_mounts
);
202 pub fn forward_space_count_files(&mut self, count
: usize) -> Result
<(), Error
> {
203 self.sg_tape
.space_filemarks(count
.try_into()?
)
206 pub fn backward_space_count_files(&mut self, count
: usize) -> Result
<(), Error
> {
207 self.sg_tape
.space_filemarks(-count
.try_into()?
)
210 pub fn forward_space_count_records(&mut self, count
: usize) -> Result
<(), Error
> {
211 self.sg_tape
.space_blocks(count
.try_into()?
)
214 pub fn backward_space_count_records(&mut self, count
: usize) -> Result
<(), Error
> {
215 self.sg_tape
.space_blocks(-count
.try_into()?
)
218 /// Position the tape after filemark count. Count 0 means BOT.
220 /// Note: we dont use LOCATE(10), because that needs LTO5
221 pub fn locate_file(&mut self, position
: u64) -> Result
<(), Error
> {
224 return self.rewind();
227 let current_position
= self.current_file_number()?
;
229 if current_position
== position
{
230 // make sure we are immediated afer the filemark
231 self.sg_tape
.space_filemarks(-1)?
;
232 self.sg_tape
.space_filemarks(1)?
;
233 } else if current_position
< position
{
234 let diff
= position
- current_position
;
235 self.sg_tape
.space_filemarks(diff
.try_into()?
)?
;
237 let diff
= current_position
- position
+ 1;
238 self.sg_tape
.space_filemarks(-diff
.try_into()?
)?
;
239 // move to EOT side of filemark
240 self.sg_tape
.space_filemarks(1)?
;
246 pub fn erase_media(&mut self, fast
: bool
) -> Result
<(), Error
> {
247 self.sg_tape
.erase_media(fast
)
250 pub fn load(&mut self) -> Result
<(), Error
> {
254 /// Read Cartridge Memory (MAM Attributes)
255 pub fn cartridge_memory(&mut self) -> Result
<Vec
<MamAttribute
>, Error
> {
256 self.sg_tape
.cartridge_memory()
259 /// Read Volume Statistics
260 pub fn volume_statistics(&mut self) -> Result
<Lp17VolumeStatistics
, Error
> {
261 self.sg_tape
.volume_statistics()
264 /// Lock the drive door
265 pub fn lock(&mut self) -> Result
<(), Error
> {
266 self.sg_tape
.set_medium_removal(false)
267 .map_err(|err
| format_err
!("lock door failed - {}", err
))
270 /// Unlock the drive door
271 pub fn unlock(&mut self) -> Result
<(), Error
> {
272 self.sg_tape
.set_medium_removal(true)
273 .map_err(|err
| format_err
!("unlock door failed - {}", err
))
278 impl TapeDriver
for LtoTapeHandle
{
280 fn sync(&mut self) -> Result
<(), Error
> {
281 self.sg_tape
.sync()?
;
285 /// Go to the end of the recorded media (for appending files).
286 fn move_to_eom(&mut self, write_missing_eof
: bool
) -> Result
<(), Error
> {
287 self.sg_tape
.move_to_eom(write_missing_eof
)
290 fn move_to_last_file(&mut self) -> Result
<(), Error
> {
292 self.move_to_eom(false)?
;
294 self.sg_tape
.check_filemark()?
;
296 let pos
= self.current_file_number()?
;
299 bail
!("move_to_last_file failed - media contains no data");
307 self.backward_space_count_files(2)?
;
308 self.forward_space_count_files(1)?
;
313 fn rewind(&mut self) -> Result
<(), Error
> {
314 self.sg_tape
.rewind()
317 fn current_file_number(&mut self) -> Result
<u64, Error
> {
318 self.sg_tape
.current_file_number()
321 fn format_media(&mut self, fast
: bool
) -> Result
<(), Error
> {
322 self.sg_tape
.format_media(fast
)
325 fn read_next_file
<'a
>(&'a
mut self) -> Result
<Option
<Box
<dyn TapeRead
+ 'a
>>, std
::io
::Error
> {
326 let reader
= self.sg_tape
.open_reader()?
;
327 let handle
= match reader
{
329 let reader
: Box
<dyn TapeRead
> = Box
::new(reader
);
338 fn write_file
<'a
>(&'a
mut self) -> Result
<Box
<dyn TapeWrite
+ 'a
>, std
::io
::Error
> {
339 let handle
= self.sg_tape
.open_writer();
343 fn write_media_set_label(
345 media_set_label
: &MediaSetLabel
,
346 key_config
: Option
<&KeyConfig
>,
347 ) -> Result
<(), Error
> {
349 let file_number
= self.current_file_number()?
;
350 if file_number
!= 1 {
352 self.forward_space_count_files(1)?
; // skip label
355 let file_number
= self.current_file_number()?
;
356 if file_number
!= 1 {
357 bail
!("write_media_set_label failed - got wrong file number ({} != 1)", file_number
);
360 self.set_encryption(None
)?
;
362 { // limit handle scope
363 let mut handle
= self.write_file()?
;
365 let mut value
= serde_json
::to_value(media_set_label
)?
;
366 if media_set_label
.encryption_key_fingerprint
.is_some() {
368 Some(key_config
) => {
369 value
["key-config"] = serde_json
::to_value(key_config
)?
;
372 bail
!("missing encryption key config");
377 let raw
= serde_json
::to_string_pretty(&value
)?
;
379 let header
= MediaContentHeader
::new(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0
, raw
.len() as u32);
380 handle
.write_header(&header
, raw
.as_bytes())?
;
381 handle
.finish(false)?
;
384 self.sync()?
; // sync data to tape
389 /// Rewind and put the drive off line (Eject media).
390 fn eject_media(&mut self) -> Result
<(), Error
> {
394 /// Read Tape Alert Flags
395 fn tape_alert_flags(&mut self) -> Result
<TapeAlertFlags
, Error
> {
396 self.sg_tape
.tape_alert_flags()
399 /// Set or clear encryption key
401 /// Note: Only 'root' can read secret encryption keys, so we need
402 /// to spawn setuid binary 'sg-tape-cmd'.
405 key_fingerprint
: Option
<(Fingerprint
, Uuid
)>,
406 ) -> Result
<(), Error
> {
408 if nix
::unistd
::Uid
::effective().is_root() {
410 if let Some((ref key_fingerprint
, ref uuid
)) = key_fingerprint
{
412 let (key_map
, _digest
) = config
::tape_encryption_keys
::load_keys()?
;
413 match key_map
.get(key_fingerprint
) {
416 // derive specialized key for each media-set
418 let mut tape_key
= [0u8; 32];
420 let uuid_bytes
: [u8; 16] = uuid
.as_bytes().clone();
422 openssl
::pkcs5
::pbkdf2_hmac(
426 openssl
::hash
::MessageDigest
::sha256(),
429 return self.sg_tape
.set_encryption(Some(tape_key
));
431 None
=> bail
!("unknown tape encryption key '{}'", key_fingerprint
),
434 return self.sg_tape
.set_encryption(None
);
438 let output
= if let Some((fingerprint
, uuid
)) = key_fingerprint
{
439 let fingerprint
= crate::tools
::format
::as_fingerprint(fingerprint
.bytes());
440 run_sg_tape_cmd("encryption", &[
441 "--fingerprint", &fingerprint
,
442 "--uuid", &uuid
.to_string(),
443 ], self.sg_tape
.file_mut().as_raw_fd())?
445 run_sg_tape_cmd("encryption", &[], self.sg_tape
.file_mut().as_raw_fd())?
447 let result
: Result
<(), String
> = serde_json
::from_str(&output
)?
;
448 result
.map_err(|err
| format_err
!("{}", err
))
452 /// Check for correct Major/Minor numbers
453 pub fn check_tape_is_lto_tape_device(file
: &File
) -> Result
<(), Error
> {
455 let stat
= nix
::sys
::stat
::fstat(file
.as_raw_fd())?
;
457 let devnum
= stat
.st_rdev
;
459 let major
= unsafe { libc::major(devnum) }
;
460 let _minor
= unsafe { libc::minor(devnum) }
;
463 bail
!("not a scsi-generic tape device (cannot use linux tape devices)");
467 bail
!("not a scsi-generic tape device");
473 /// Opens a Lto tape device
475 /// The open call use O_NONBLOCK, but that flag is cleard after open
476 /// succeeded. This also checks if the device is a non-rewinding tape
478 pub fn open_lto_tape_device(
480 ) -> Result
<File
, Error
> {
482 let file
= OpenOptions
::new()
485 .custom_flags(libc
::O_NONBLOCK
)
488 // clear O_NONBLOCK from now on.
490 let flags
= fcntl(file
.as_raw_fd(), FcntlArg
::F_GETFL
)
493 let mut flags
= OFlag
::from_bits_truncate(flags
);
494 flags
.remove(OFlag
::O_NONBLOCK
);
496 fcntl(file
.as_raw_fd(), FcntlArg
::F_SETFL(flags
))
499 check_tape_is_lto_tape_device(&file
)
500 .map_err(|err
| format_err
!("device type check {:?} failed - {}", path
, err
))?
;
505 fn run_sg_tape_cmd(subcmd
: &str, args
: &[&str], fd
: RawFd
) -> Result
<String
, Error
> {
506 let mut command
= std
::process
::Command
::new(
507 "/usr/lib/x86_64-linux-gnu/proxmox-backup/sg-tape-cmd");
508 command
.args(&[subcmd
]);
509 command
.args(&["--stdin"]);
511 let device_fd
= nix
::unistd
::dup(fd
)?
;
512 command
.stdin(unsafe { std::process::Stdio::from_raw_fd(device_fd)}
);
513 run_command(command
, None
)