1 use std
::fs
::{OpenOptions, File}
;
2 use std
::os
::unix
::fs
::OpenOptionsExt
;
3 use std
::os
::unix
::io
::{AsRawFd, FromRawFd}
;
4 use std
::convert
::TryFrom
;
6 use anyhow
::{bail, format_err, Error}
;
7 use nix
::fcntl
::{fcntl, FcntlArg, OFlag}
;
9 use proxmox
::sys
::error
::SysResult
;
21 LinuxDriveAndMediaStatus
,
29 mam_extract_media_usage
,
30 read_tape_alert_flags
,
31 read_volume_statistics
,
39 PROXMOX_TAPE_BLOCK_SIZE
,
42 PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0
,
52 pub struct LinuxDriveStatus
{
54 pub status
: GMTStatusFlags
,
55 pub density
: Option
<TapeDensity
>,
56 pub file_number
: Option
<u32>,
57 pub block_number
: Option
<u32>,
60 impl LinuxDriveStatus
{
61 pub fn tape_is_ready(&self) -> bool
{
62 self.status
.contains(GMTStatusFlags
::ONLINE
) &&
63 !self.status
.contains(GMTStatusFlags
::DRIVE_OPEN
)
69 /// Open a tape device
71 /// This does additional checks:
73 /// - check if it is a non-rewinding tape device
74 /// - check if drive is ready (tape loaded)
75 /// - check block size
76 /// - for autoloader only, try to reload ejected tapes
77 pub fn open(&self) -> Result
<LinuxTapeHandle
, Error
> {
80 let file
= open_linux_tape_device(&self.path
)?
;
82 let mut handle
= LinuxTapeHandle
::new(file
);
84 let mut drive_status
= handle
.get_drive_status()?
;
86 if !drive_status
.tape_is_ready() {
87 // for autoloader only, try to reload ejected tapes
88 if self.changer
.is_some() {
89 let _
= handle
.mtload(); // just try, ignore error
90 drive_status
= handle
.get_drive_status()?
;
94 if !drive_status
.tape_is_ready() {
95 bail
!("tape not ready (no tape loaded)");
98 if drive_status
.blocksize
== 0 {
99 // device is variable block size - OK
101 if drive_status
.blocksize
!= PROXMOX_TAPE_BLOCK_SIZE
as u32 {
102 eprintln
!("device is in fixed block size mode with wrong size ({} bytes)", drive_status
.blocksize
);
103 eprintln
!("trying to set variable block size mode...");
104 if handle
.set_block_size(0).is_err() {
105 bail
!("set variable block size mod failed - device uses wrong blocksize.");
108 // device is in fixed block size mode with correct block size
112 // Only root can set driver options, so we cannot
113 // handle.set_default_options()?;
116 }).map_err(|err
| format_err
!("open drive '{}' ({}) failed - {}", self.name
, self.path
, err
))
120 /// Linux Tape device handle
121 pub struct LinuxTapeHandle
{
126 impl LinuxTapeHandle
{
128 /// Creates a new instance
129 pub fn new(file
: File
) -> Self {
133 /// Set all options we need/want
134 pub fn set_default_options(&self) -> Result
<(), Error
> {
136 let mut opts
= SetDrvBufferOptions
::empty();
138 // fixme: ? man st(4) claims we need to clear this for reliable multivolume
139 opts
.set(SetDrvBufferOptions
::MT_ST_BUFFER_WRITES
, true);
141 // fixme: ?man st(4) claims we need to clear this for reliable multivolume
142 opts
.set(SetDrvBufferOptions
::MT_ST_ASYNC_WRITES
, true);
144 opts
.set(SetDrvBufferOptions
::MT_ST_READ_AHEAD
, true);
146 self.set_drive_buffer_options(opts
)
149 /// call MTSETDRVBUFFER to set boolean options
151 /// Note: this uses MT_ST_BOOLEANS, so missing options are cleared!
152 pub fn set_drive_buffer_options(&self, opts
: SetDrvBufferOptions
) -> Result
<(), Error
> {
155 mt_op
: MTCmd
::MTSETDRVBUFFER
,
156 mt_count
: (SetDrvBufferCmd
::MT_ST_BOOLEANS
as i32) | opts
.bits(),
159 mtioctop(self.file
.as_raw_fd(), &cmd
)
160 }.map_err(|err
| format_err
!("MTSETDRVBUFFER options failed - {}", err
))?
;
165 /// This flushes the driver's buffer as a side effect. Should be
166 /// used before reading status with MTIOCGET.
167 fn mtnop(&self) -> Result
<(), Error
> {
169 let cmd
= mtop { mt_op: MTCmd::MTNOP, mt_count: 1, }
;
172 mtioctop(self.file
.as_raw_fd(), &cmd
)
173 }.map_err(|err
| format_err
!("MTNOP failed - {}", err
))?
;
178 fn mtload(&mut self) -> Result
<(), Error
> {
180 let cmd
= mtop { mt_op: MTCmd::MTLOAD, mt_count: 1, }
;
183 mtioctop(self.file
.as_raw_fd(), &cmd
)
184 }.map_err(|err
| format_err
!("MTLOAD failed - {}", err
))?
;
189 fn forward_space_count_files(&mut self, count
: i32) -> Result
<(), Error
> {
191 let cmd
= mtop { mt_op: MTCmd::MTFSF, mt_count: count, }
;
194 mtioctop(self.file
.as_raw_fd(), &cmd
)
195 }.map_err(|err
| format_err
!("tape fsf {} failed - {}", count
, err
))?
;
200 /// Set tape compression feature
201 pub fn set_compression(&self, on
: bool
) -> Result
<(), Error
> {
203 let cmd
= mtop { mt_op: MTCmd::MTCOMPRESSION, mt_count: if on { 1 }
else { 0 }
};
206 mtioctop(self.file
.as_raw_fd(), &cmd
)
207 }.map_err(|err
| format_err
!("set compression to {} failed - {}", on
, err
))?
;
212 /// Write a single EOF mark
213 pub fn write_eof_mark(&self) -> Result
<(), Error
> {
214 tape_write_eof_mark(&self.file
)?
;
218 /// Set the drive's block length to the value specified.
220 /// A block length of zero sets the drive to variable block
222 pub fn set_block_size(&self, block_length
: usize) -> Result
<(), Error
> {
224 if block_length
> 256*1024*1024 {
225 bail
!("block_length too large (> max linux scsii block length)");
228 let cmd
= mtop { mt_op: MTCmd::MTSETBLK, mt_count: block_length as i32 }
;
231 mtioctop(self.file
.as_raw_fd(), &cmd
)
232 }.map_err(|err
| format_err
!("MTSETBLK failed - {}", err
))?
;
237 /// Get Tape and Media status
238 pub fn get_drive_and_media_status(&mut self) -> Result
<LinuxDriveAndMediaStatus
, Error
> {
240 let drive_status
= self.get_drive_status()?
;
242 let alert_flags
= self.tape_alert_flags()
243 .map(|flags
| format
!("{:?}", flags
))
246 let mut status
= LinuxDriveAndMediaStatus
{
247 blocksize
: drive_status
.blocksize
,
248 density
: drive_status
.density
,
249 status
: format
!("{:?}", drive_status
.status
),
251 file_number
: drive_status
.file_number
,
252 block_number
: drive_status
.block_number
,
260 if drive_status
.tape_is_ready() {
262 if let Ok(mam
) = self.cartridge_memory() {
264 let usage
= mam_extract_media_usage(&mam
)?
;
266 status
.manufactured
= Some(usage
.manufactured
);
267 status
.bytes_read
= Some(usage
.bytes_read
);
268 status
.bytes_written
= Some(usage
.bytes_written
);
270 if let Ok(volume_stats
) = self.volume_statistics() {
272 status
.medium_passes
= Some(std
::cmp
::max(
273 volume_stats
.beginning_of_medium_passes
,
274 volume_stats
.middle_of_tape_passes
,
277 status
.volume_mounts
= Some(volume_stats
.volume_mounts
);
285 /// Get Tape status/configuration with MTIOCGET ioctl
286 pub fn get_drive_status(&mut self) -> Result
<LinuxDriveStatus
, Error
> {
288 let _
= self.mtnop(); // ignore errors (i.e. no tape loaded)
290 let mut status
= mtget
::default();
292 if let Err(err
) = unsafe { mtiocget(self.file.as_raw_fd(), &mut status) }
{
293 bail
!("MTIOCGET failed - {}", err
);
296 let gmt
= GMTStatusFlags
::from_bits_truncate(status
.mt_gstat
);
300 if status
.mt_type
== MT_TYPE_ISSCSI1
|| status
.mt_type
== MT_TYPE_ISSCSI2
{
301 blocksize
= ((status
.mt_dsreg
& MT_ST_BLKSIZE_MASK
) >> MT_ST_BLKSIZE_SHIFT
) as u32;
303 bail
!("got unsupported tape type {}", status
.mt_type
);
306 let density
= ((status
.mt_dsreg
& MT_ST_DENSITY_MASK
) >> MT_ST_DENSITY_SHIFT
) as u8;
308 Ok(LinuxDriveStatus
{
311 density
: if density
!= 0 {
312 Some(TapeDensity
::try_from(density
)?
)
316 file_number
: if status
.mt_fileno
> 0 {
317 Some(status
.mt_fileno
as u32)
321 block_number
: if status
.mt_blkno
> 0 {
322 Some(status
.mt_blkno
as u32)
329 /// Read Cartridge Memory (MAM Attributes)
331 /// Note: Only 'root' user may run RAW SG commands, so we need to
332 /// spawn setuid binary 'sg-tape-cmd'.
333 pub fn cartridge_memory(&mut self) -> Result
<Vec
<MamAttribute
>, Error
> {
335 if nix
::unistd
::Uid
::effective().is_root() {
336 return read_mam_attributes(&mut self.file
);
339 let mut command
= std
::process
::Command
::new(
340 "/usr/lib/x86_64-linux-gnu/proxmox-backup/sg-tape-cmd");
341 command
.args(&["cartridge-memory"]);
342 command
.args(&["--stdin"]);
343 command
.stdin(unsafe { std::process::Stdio::from_raw_fd(self.file.as_raw_fd())}
);
344 let output
= run_command(command
, None
)?
;
345 let result
: Result
<Vec
<MamAttribute
>, String
> = serde_json
::from_str(&output
)?
;
346 result
.map_err(|err
| format_err
!("{}", err
))
349 /// Read Volume Statistics
351 /// Note: Only 'root' user may run RAW SG commands, so we need to
352 /// spawn setuid binary 'sg-tape-cmd'.
353 pub fn volume_statistics(&mut self) -> Result
<Lp17VolumeStatistics
, Error
> {
355 if nix
::unistd
::Uid
::effective().is_root() {
356 return read_volume_statistics(&mut self.file
);
359 let mut command
= std
::process
::Command
::new(
360 "/usr/lib/x86_64-linux-gnu/proxmox-backup/sg-tape-cmd");
361 command
.args(&["volume-statistics"]);
362 command
.args(&["--stdin"]);
363 command
.stdin(unsafe { std::process::Stdio::from_raw_fd(self.file.as_raw_fd())}
);
364 let output
= run_command(command
, None
)?
;
365 let result
: Result
<Lp17VolumeStatistics
, String
> = serde_json
::from_str(&output
)?
;
366 result
.map_err(|err
| format_err
!("{}", err
))
371 impl TapeDriver
for LinuxTapeHandle
{
373 fn sync(&mut self) -> Result
<(), Error
> {
375 println
!("SYNC/FLUSH TAPE");
376 // MTWEOF with count 0 => flush
377 let cmd
= mtop { mt_op: MTCmd::MTWEOF, mt_count: 0 }
;
380 mtioctop(self.file
.as_raw_fd(), &cmd
)
381 }.map_err(|err
| proxmox
::io_format_err
!("MT sync failed - {}", err
))?
;
386 /// Go to the end of the recorded media (for appending files).
387 fn move_to_eom(&mut self) -> Result
<(), Error
> {
389 let cmd
= mtop { mt_op: MTCmd::MTEOM, mt_count: 1, }
;
392 mtioctop(self.file
.as_raw_fd(), &cmd
)
393 }.map_err(|err
| format_err
!("MTEOM failed - {}", err
))?
;
399 fn rewind(&mut self) -> Result
<(), Error
> {
401 let cmd
= mtop { mt_op: MTCmd::MTREW, mt_count: 1, }
;
404 mtioctop(self.file
.as_raw_fd(), &cmd
)
405 }.map_err(|err
| format_err
!("tape rewind failed - {}", err
))?
;
410 fn current_file_number(&mut self) -> Result
<u64, Error
> {
411 let mut status
= mtget
::default();
415 if let Err(err
) = unsafe { mtiocget(self.file.as_raw_fd(), &mut status) }
{
416 bail
!("current_file_number MTIOCGET failed - {}", err
);
419 if status
.mt_fileno
< 0 {
420 bail
!("current_file_number failed (got {})", status
.mt_fileno
);
422 Ok(status
.mt_fileno
as u64)
425 fn erase_media(&mut self, fast
: bool
) -> Result
<(), Error
> {
427 self.rewind()?
; // important - erase from BOT
429 let cmd
= mtop { mt_op: MTCmd::MTERASE, mt_count: if fast { 0 }
else { 1 }
};
432 mtioctop(self.file
.as_raw_fd(), &cmd
)
433 }.map_err(|err
| format_err
!("MTERASE failed - {}", err
))?
;
438 fn read_next_file
<'a
>(&'a
mut self) -> Result
<Option
<Box
<dyn TapeRead
+ 'a
>>, std
::io
::Error
> {
439 match BlockedReader
::open(&mut self.file
)?
{
440 Some(reader
) => Ok(Some(Box
::new(reader
))),
445 fn write_file
<'a
>(&'a
mut self) -> Result
<Box
<dyn TapeWrite
+ 'a
>, std
::io
::Error
> {
447 let handle
= TapeWriterHandle
{
448 writer
: BlockedWriter
::new(&mut self.file
),
454 fn write_media_set_label(
456 media_set_label
: &MediaSetLabel
,
457 key_config
: Option
<&KeyConfig
>,
458 ) -> Result
<(), Error
> {
460 let file_number
= self.current_file_number()?
;
461 if file_number
!= 1 {
463 self.forward_space_count_files(1)?
; // skip label
466 let file_number
= self.current_file_number()?
;
467 if file_number
!= 1 {
468 bail
!("write_media_set_label failed - got wrong file number ({} != 1)", file_number
);
471 self.set_encryption(None
)?
;
473 let mut handle
= TapeWriterHandle
{
474 writer
: BlockedWriter
::new(&mut self.file
),
477 let mut value
= serde_json
::to_value(media_set_label
)?
;
478 if media_set_label
.encryption_key_fingerprint
.is_some() {
480 Some(key_config
) => {
481 value
["key-config"] = serde_json
::to_value(key_config
)?
;
484 bail
!("missing encryption key config");
489 let raw
= serde_json
::to_string_pretty(&value
)?
;
491 let header
= MediaContentHeader
::new(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0
, raw
.len() as u32);
492 handle
.write_header(&header
, raw
.as_bytes())?
;
493 handle
.finish(false)?
;
495 self.sync()?
; // sync data to tape
500 /// Rewind and put the drive off line (Eject media).
501 fn eject_media(&mut self) -> Result
<(), Error
> {
502 let cmd
= mtop { mt_op: MTCmd::MTOFFL, mt_count: 1 }
;
505 mtioctop(self.file
.as_raw_fd(), &cmd
)
506 }.map_err(|err
| format_err
!("MTOFFL failed - {}", err
))?
;
511 /// Read Tape Alert Flags
513 /// Note: Only 'root' user may run RAW SG commands, so we need to
514 /// spawn setuid binary 'sg-tape-cmd'.
515 fn tape_alert_flags(&mut self) -> Result
<TapeAlertFlags
, Error
> {
517 if nix
::unistd
::Uid
::effective().is_root() {
518 return read_tape_alert_flags(&mut self.file
);
521 let mut command
= std
::process
::Command
::new(
522 "/usr/lib/x86_64-linux-gnu/proxmox-backup/sg-tape-cmd");
523 command
.args(&["tape-alert-flags"]);
524 command
.args(&["--stdin"]);
525 command
.stdin(unsafe { std::process::Stdio::from_raw_fd(self.file.as_raw_fd())}
);
526 let output
= run_command(command
, None
)?
;
527 let result
: Result
<u64, String
> = serde_json
::from_str(&output
)?
;
529 .map_err(|err
| format_err
!("{}", err
))
530 .map(TapeAlertFlags
::from_bits_truncate
)
533 /// Set or clear encryption key
535 /// Note: Only 'root' user may run RAW SG commands, so we need to
536 /// spawn setuid binary 'sg-tape-cmd'. Also, encryption key file
537 /// is only readable by root.
538 fn set_encryption(&mut self, key_fingerprint
: Option
<Fingerprint
>) -> Result
<(), Error
> {
540 if nix
::unistd
::Uid
::effective().is_root() {
542 if let Some(ref key_fingerprint
) = key_fingerprint
{
544 let (key_map
, _digest
) = config
::tape_encryption_keys
::load_keys()?
;
545 match key_map
.get(key_fingerprint
) {
547 return set_encryption(&mut self.file
, Some(item
.key
));
549 None
=> bail
!("unknown tape encryption key '{}'", key_fingerprint
),
552 return set_encryption(&mut self.file
, None
);
556 let mut command
= std
::process
::Command
::new(
557 "/usr/lib/x86_64-linux-gnu/proxmox-backup/sg-tape-cmd");
558 command
.args(&["encryption"]);
559 if let Some(fingerprint
) = key_fingerprint
{
560 let fingerprint
= crate::tools
::format
::as_fingerprint(fingerprint
.bytes());
561 command
.args(&["--fingerprint", &fingerprint
]);
563 command
.args(&["--stdin"]);
564 command
.stdin(unsafe { std::process::Stdio::from_raw_fd(self.file.as_raw_fd())}
);
565 let output
= run_command(command
, None
)?
;
566 let result
: Result
<(), String
> = serde_json
::from_str(&output
)?
;
567 result
.map_err(|err
| format_err
!("{}", err
))
571 /// Write a single EOF mark without flushing buffers
572 fn tape_write_eof_mark(file
: &File
) -> Result
<(), std
::io
::Error
> {
574 println
!("WRITE EOF MARK");
575 let cmd
= mtop { mt_op: MTCmd::MTWEOFI, mt_count: 1 }
;
578 mtioctop(file
.as_raw_fd(), &cmd
)
579 }.map_err(|err
| proxmox
::io_format_err
!("MTWEOFI failed - {}", err
))?
;
584 /// Check for correct Major/Minor numbers
585 pub fn check_tape_is_linux_tape_device(file
: &File
) -> Result
<(), Error
> {
587 let stat
= nix
::sys
::stat
::fstat(file
.as_raw_fd())?
;
589 let devnum
= stat
.st_rdev
;
591 let major
= unsafe { libc::major(devnum) }
;
592 let minor
= unsafe { libc::minor(devnum) }
;
595 bail
!("not a tape device");
597 if (minor
& 128) == 0 {
598 bail
!("Detected rewinding tape. Please use non-rewinding tape devices (/dev/nstX).");
604 /// Opens a Linux tape device
606 /// The open call use O_NONBLOCK, but that flag is cleard after open
607 /// succeeded. This also checks if the device is a non-rewinding tape
609 pub fn open_linux_tape_device(
611 ) -> Result
<File
, Error
> {
613 let file
= OpenOptions
::new()
616 .custom_flags(libc
::O_NONBLOCK
)
619 // clear O_NONBLOCK from now on.
621 let flags
= fcntl(file
.as_raw_fd(), FcntlArg
::F_GETFL
)
624 let mut flags
= OFlag
::from_bits_truncate(flags
);
625 flags
.remove(OFlag
::O_NONBLOCK
);
627 fcntl(file
.as_raw_fd(), FcntlArg
::F_SETFL(flags
))
630 check_tape_is_linux_tape_device(&file
)
631 .map_err(|err
| format_err
!("device type check {:?} failed - {}", path
, err
))?
;
636 /// like BlockedWriter, but writes EOF mark on finish
637 pub struct TapeWriterHandle
<'a
> {
638 writer
: BlockedWriter
<&'a
mut File
>,
641 impl TapeWrite
for TapeWriterHandle
<'_
> {
643 fn write_all(&mut self, data
: &[u8]) -> Result
<bool
, std
::io
::Error
> {
644 self.writer
.write_all(data
)
647 fn bytes_written(&self) -> usize {
648 self.writer
.bytes_written()
651 fn finish(&mut self, incomplete
: bool
) -> Result
<bool
, std
::io
::Error
> {
652 println
!("FINISH TAPE HANDLE");
653 let leof
= self.writer
.finish(incomplete
)?
;
654 tape_write_eof_mark(self.writer
.writer_ref_mut())?
;
658 fn logical_end_of_media(&self) -> bool
{
659 self.writer
.logical_end_of_media()