]> git.proxmox.com Git - proxmox-backup.git/blob - src/tape/drive/linux_tape.rs
clippy: remove unnecessary closures
[proxmox-backup.git] / src / tape / drive / linux_tape.rs
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;
5
6 use anyhow::{bail, format_err, Error};
7 use nix::fcntl::{fcntl, FcntlArg, OFlag};
8
9 use proxmox::sys::error::SysResult;
10
11 use crate::{
12 config,
13 backup::{
14 Fingerprint,
15 KeyConfig,
16 },
17 tools::run_command,
18 api2::types::{
19 TapeDensity,
20 MamAttribute,
21 LinuxDriveAndMediaStatus,
22 },
23 tape::{
24 TapeRead,
25 TapeWrite,
26 TapeAlertFlags,
27 Lp17VolumeStatistics,
28 read_mam_attributes,
29 mam_extract_media_usage,
30 read_tape_alert_flags,
31 read_volume_statistics,
32 set_encryption,
33 drive::{
34 LinuxTapeDrive,
35 TapeDriver,
36 linux_mtio::*,
37 },
38 file_formats::{
39 PROXMOX_TAPE_BLOCK_SIZE,
40 MediaSetLabel,
41 MediaContentHeader,
42 PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0,
43 },
44 helpers::{
45 BlockedReader,
46 BlockedWriter,
47 },
48 }
49 };
50
51 #[derive(Debug)]
52 pub struct LinuxDriveStatus {
53 pub blocksize: u32,
54 pub status: GMTStatusFlags,
55 pub density: Option<TapeDensity>,
56 pub file_number: Option<u32>,
57 pub block_number: Option<u32>,
58 }
59
60 impl LinuxDriveStatus {
61 pub fn tape_is_ready(&self) -> bool {
62 self.status.contains(GMTStatusFlags::ONLINE) &&
63 !self.status.contains(GMTStatusFlags::DRIVE_OPEN)
64 }
65 }
66
67 impl LinuxTapeDrive {
68
69 /// Open a tape device
70 ///
71 /// This does additional checks:
72 ///
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> {
78
79 proxmox::try_block!({
80 let file = open_linux_tape_device(&self.path)?;
81
82 let mut handle = LinuxTapeHandle::new(file);
83
84 let mut drive_status = handle.get_drive_status()?;
85
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()?;
91 }
92 }
93
94 if !drive_status.tape_is_ready() {
95 bail!("tape not ready (no tape loaded)");
96 }
97
98 if drive_status.blocksize == 0 {
99 // device is variable block size - OK
100 } else {
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.");
106 }
107 } else {
108 // device is in fixed block size mode with correct block size
109 }
110 }
111
112 // Only root can set driver options, so we cannot
113 // handle.set_default_options()?;
114
115 Ok(handle)
116 }).map_err(|err| format_err!("open drive '{}' ({}) failed - {}", self.name, self.path, err))
117 }
118 }
119
120 /// Linux Tape device handle
121 pub struct LinuxTapeHandle {
122 file: File,
123 //_lock: File,
124 }
125
126 impl LinuxTapeHandle {
127
128 /// Creates a new instance
129 pub fn new(file: File) -> Self {
130 Self { file }
131 }
132
133 /// Set all options we need/want
134 pub fn set_default_options(&self) -> Result<(), Error> {
135
136 let mut opts = SetDrvBufferOptions::empty();
137
138 // fixme: ? man st(4) claims we need to clear this for reliable multivolume
139 opts.set(SetDrvBufferOptions::MT_ST_BUFFER_WRITES, true);
140
141 // fixme: ?man st(4) claims we need to clear this for reliable multivolume
142 opts.set(SetDrvBufferOptions::MT_ST_ASYNC_WRITES, true);
143
144 opts.set(SetDrvBufferOptions::MT_ST_READ_AHEAD, true);
145
146 self.set_drive_buffer_options(opts)
147 }
148
149 /// call MTSETDRVBUFFER to set boolean options
150 ///
151 /// Note: this uses MT_ST_BOOLEANS, so missing options are cleared!
152 pub fn set_drive_buffer_options(&self, opts: SetDrvBufferOptions) -> Result<(), Error> {
153
154 let cmd = mtop {
155 mt_op: MTCmd::MTSETDRVBUFFER,
156 mt_count: (SetDrvBufferCmd::MT_ST_BOOLEANS as i32) | opts.bits(),
157 };
158 unsafe {
159 mtioctop(self.file.as_raw_fd(), &cmd)
160 }.map_err(|err| format_err!("MTSETDRVBUFFER options failed - {}", err))?;
161
162 Ok(())
163 }
164
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> {
168
169 let cmd = mtop { mt_op: MTCmd::MTNOP, mt_count: 1, };
170
171 unsafe {
172 mtioctop(self.file.as_raw_fd(), &cmd)
173 }.map_err(|err| format_err!("MTNOP failed - {}", err))?;
174
175 Ok(())
176 }
177
178 fn mtload(&mut self) -> Result<(), Error> {
179
180 let cmd = mtop { mt_op: MTCmd::MTLOAD, mt_count: 1, };
181
182 unsafe {
183 mtioctop(self.file.as_raw_fd(), &cmd)
184 }.map_err(|err| format_err!("MTLOAD failed - {}", err))?;
185
186 Ok(())
187 }
188
189 fn forward_space_count_files(&mut self, count: i32) -> Result<(), Error> {
190
191 let cmd = mtop { mt_op: MTCmd::MTFSF, mt_count: count, };
192
193 unsafe {
194 mtioctop(self.file.as_raw_fd(), &cmd)
195 }.map_err(|err| format_err!("tape fsf {} failed - {}", count, err))?;
196
197 Ok(())
198 }
199
200 /// Set tape compression feature
201 pub fn set_compression(&self, on: bool) -> Result<(), Error> {
202
203 let cmd = mtop { mt_op: MTCmd::MTCOMPRESSION, mt_count: if on { 1 } else { 0 } };
204
205 unsafe {
206 mtioctop(self.file.as_raw_fd(), &cmd)
207 }.map_err(|err| format_err!("set compression to {} failed - {}", on, err))?;
208
209 Ok(())
210 }
211
212 /// Write a single EOF mark
213 pub fn write_eof_mark(&self) -> Result<(), Error> {
214 tape_write_eof_mark(&self.file)?;
215 Ok(())
216 }
217
218 /// Set the drive's block length to the value specified.
219 ///
220 /// A block length of zero sets the drive to variable block
221 /// size mode.
222 pub fn set_block_size(&self, block_length: usize) -> Result<(), Error> {
223
224 if block_length > 256*1024*1024 {
225 bail!("block_length too large (> max linux scsii block length)");
226 }
227
228 let cmd = mtop { mt_op: MTCmd::MTSETBLK, mt_count: block_length as i32 };
229
230 unsafe {
231 mtioctop(self.file.as_raw_fd(), &cmd)
232 }.map_err(|err| format_err!("MTSETBLK failed - {}", err))?;
233
234 Ok(())
235 }
236
237 /// Get Tape and Media status
238 pub fn get_drive_and_media_status(&mut self) -> Result<LinuxDriveAndMediaStatus, Error> {
239
240 let drive_status = self.get_drive_status()?;
241
242 let alert_flags = self.tape_alert_flags()
243 .map(|flags| format!("{:?}", flags))
244 .ok();
245
246 let mut status = LinuxDriveAndMediaStatus {
247 blocksize: drive_status.blocksize,
248 density: drive_status.density,
249 status: format!("{:?}", drive_status.status),
250 alert_flags,
251 file_number: drive_status.file_number,
252 block_number: drive_status.block_number,
253 manufactured: None,
254 bytes_read: None,
255 bytes_written: None,
256 medium_passes: None,
257 volume_mounts: None,
258 };
259
260 if drive_status.tape_is_ready() {
261
262 if let Ok(mam) = self.cartridge_memory() {
263
264 let usage = mam_extract_media_usage(&mam)?;
265
266 status.manufactured = Some(usage.manufactured);
267 status.bytes_read = Some(usage.bytes_read);
268 status.bytes_written = Some(usage.bytes_written);
269
270 if let Ok(volume_stats) = self.volume_statistics() {
271
272 status.medium_passes = Some(std::cmp::max(
273 volume_stats.beginning_of_medium_passes,
274 volume_stats.middle_of_tape_passes,
275 ));
276
277 status.volume_mounts = Some(volume_stats.volume_mounts);
278 }
279 }
280 }
281
282 Ok(status)
283 }
284
285 /// Get Tape status/configuration with MTIOCGET ioctl
286 pub fn get_drive_status(&mut self) -> Result<LinuxDriveStatus, Error> {
287
288 let _ = self.mtnop(); // ignore errors (i.e. no tape loaded)
289
290 let mut status = mtget::default();
291
292 if let Err(err) = unsafe { mtiocget(self.file.as_raw_fd(), &mut status) } {
293 bail!("MTIOCGET failed - {}", err);
294 }
295
296 let gmt = GMTStatusFlags::from_bits_truncate(status.mt_gstat);
297
298 let blocksize;
299
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;
302 } else {
303 bail!("got unsupported tape type {}", status.mt_type);
304 }
305
306 let density = ((status.mt_dsreg & MT_ST_DENSITY_MASK) >> MT_ST_DENSITY_SHIFT) as u8;
307
308 Ok(LinuxDriveStatus {
309 blocksize,
310 status: gmt,
311 density: if density != 0 {
312 Some(TapeDensity::try_from(density)?)
313 } else {
314 None
315 },
316 file_number: if status.mt_fileno > 0 {
317 Some(status.mt_fileno as u32)
318 } else {
319 None
320 },
321 block_number: if status.mt_blkno > 0 {
322 Some(status.mt_blkno as u32)
323 } else {
324 None
325 },
326 })
327 }
328
329 /// Read Cartridge Memory (MAM Attributes)
330 ///
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> {
334
335 if nix::unistd::Uid::effective().is_root() {
336 return read_mam_attributes(&mut self.file);
337 }
338
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))
347 }
348
349 /// Read Volume Statistics
350 ///
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> {
354
355 if nix::unistd::Uid::effective().is_root() {
356 return read_volume_statistics(&mut self.file);
357 }
358
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))
367 }
368 }
369
370
371 impl TapeDriver for LinuxTapeHandle {
372
373 fn sync(&mut self) -> Result<(), Error> {
374
375 println!("SYNC/FLUSH TAPE");
376 // MTWEOF with count 0 => flush
377 let cmd = mtop { mt_op: MTCmd::MTWEOF, mt_count: 0 };
378
379 unsafe {
380 mtioctop(self.file.as_raw_fd(), &cmd)
381 }.map_err(|err| proxmox::io_format_err!("MT sync failed - {}", err))?;
382
383 Ok(())
384 }
385
386 /// Go to the end of the recorded media (for appending files).
387 fn move_to_eom(&mut self) -> Result<(), Error> {
388
389 let cmd = mtop { mt_op: MTCmd::MTEOM, mt_count: 1, };
390
391 unsafe {
392 mtioctop(self.file.as_raw_fd(), &cmd)
393 }.map_err(|err| format_err!("MTEOM failed - {}", err))?;
394
395
396 Ok(())
397 }
398
399 fn rewind(&mut self) -> Result<(), Error> {
400
401 let cmd = mtop { mt_op: MTCmd::MTREW, mt_count: 1, };
402
403 unsafe {
404 mtioctop(self.file.as_raw_fd(), &cmd)
405 }.map_err(|err| format_err!("tape rewind failed - {}", err))?;
406
407 Ok(())
408 }
409
410 fn current_file_number(&mut self) -> Result<u64, Error> {
411 let mut status = mtget::default();
412
413 self.mtnop()?;
414
415 if let Err(err) = unsafe { mtiocget(self.file.as_raw_fd(), &mut status) } {
416 bail!("current_file_number MTIOCGET failed - {}", err);
417 }
418
419 if status.mt_fileno < 0 {
420 bail!("current_file_number failed (got {})", status.mt_fileno);
421 }
422 Ok(status.mt_fileno as u64)
423 }
424
425 fn erase_media(&mut self, fast: bool) -> Result<(), Error> {
426
427 self.rewind()?; // important - erase from BOT
428
429 let cmd = mtop { mt_op: MTCmd::MTERASE, mt_count: if fast { 0 } else { 1 } };
430
431 unsafe {
432 mtioctop(self.file.as_raw_fd(), &cmd)
433 }.map_err(|err| format_err!("MTERASE failed - {}", err))?;
434
435 Ok(())
436 }
437
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))),
441 None => Ok(None),
442 }
443 }
444
445 fn write_file<'a>(&'a mut self) -> Result<Box<dyn TapeWrite + 'a>, std::io::Error> {
446
447 let handle = TapeWriterHandle {
448 writer: BlockedWriter::new(&mut self.file),
449 };
450
451 Ok(Box::new(handle))
452 }
453
454 fn write_media_set_label(
455 &mut self,
456 media_set_label: &MediaSetLabel,
457 key_config: Option<&KeyConfig>,
458 ) -> Result<(), Error> {
459
460 let file_number = self.current_file_number()?;
461 if file_number != 1 {
462 self.rewind()?;
463 self.forward_space_count_files(1)?; // skip label
464 }
465
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);
469 }
470
471 self.set_encryption(None)?;
472
473 let mut handle = TapeWriterHandle {
474 writer: BlockedWriter::new(&mut self.file),
475 };
476
477 let mut value = serde_json::to_value(media_set_label)?;
478 if media_set_label.encryption_key_fingerprint.is_some() {
479 match key_config {
480 Some(key_config) => {
481 value["key-config"] = serde_json::to_value(key_config)?;
482 }
483 None => {
484 bail!("missing encryption key config");
485 }
486 }
487 }
488
489 let raw = serde_json::to_string_pretty(&value)?;
490
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)?;
494
495 self.sync()?; // sync data to tape
496
497 Ok(())
498 }
499
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 };
503
504 unsafe {
505 mtioctop(self.file.as_raw_fd(), &cmd)
506 }.map_err(|err| format_err!("MTOFFL failed - {}", err))?;
507
508 Ok(())
509 }
510
511 /// Read Tape Alert Flags
512 ///
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> {
516
517 if nix::unistd::Uid::effective().is_root() {
518 return read_tape_alert_flags(&mut self.file);
519 }
520
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)?;
528 result
529 .map_err(|err| format_err!("{}", err))
530 .map(TapeAlertFlags::from_bits_truncate)
531 }
532
533 /// Set or clear encryption key
534 ///
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> {
539
540 if nix::unistd::Uid::effective().is_root() {
541
542 if let Some(ref key_fingerprint) = key_fingerprint {
543
544 let (key_map, _digest) = config::tape_encryption_keys::load_keys()?;
545 match key_map.get(key_fingerprint) {
546 Some(item) => {
547 return set_encryption(&mut self.file, Some(item.key));
548 }
549 None => bail!("unknown tape encryption key '{}'", key_fingerprint),
550 }
551 } else {
552 return set_encryption(&mut self.file, None);
553 }
554 }
555
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]);
562 }
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))
568 }
569 }
570
571 /// Write a single EOF mark without flushing buffers
572 fn tape_write_eof_mark(file: &File) -> Result<(), std::io::Error> {
573
574 println!("WRITE EOF MARK");
575 let cmd = mtop { mt_op: MTCmd::MTWEOFI, mt_count: 1 };
576
577 unsafe {
578 mtioctop(file.as_raw_fd(), &cmd)
579 }.map_err(|err| proxmox::io_format_err!("MTWEOFI failed - {}", err))?;
580
581 Ok(())
582 }
583
584 /// Check for correct Major/Minor numbers
585 pub fn check_tape_is_linux_tape_device(file: &File) -> Result<(), Error> {
586
587 let stat = nix::sys::stat::fstat(file.as_raw_fd())?;
588
589 let devnum = stat.st_rdev;
590
591 let major = unsafe { libc::major(devnum) };
592 let minor = unsafe { libc::minor(devnum) };
593
594 if major != 9 {
595 bail!("not a tape device");
596 }
597 if (minor & 128) == 0 {
598 bail!("Detected rewinding tape. Please use non-rewinding tape devices (/dev/nstX).");
599 }
600
601 Ok(())
602 }
603
604 /// Opens a Linux tape device
605 ///
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
608 /// device.
609 pub fn open_linux_tape_device(
610 path: &str,
611 ) -> Result<File, Error> {
612
613 let file = OpenOptions::new()
614 .read(true)
615 .write(true)
616 .custom_flags(libc::O_NONBLOCK)
617 .open(path)?;
618
619 // clear O_NONBLOCK from now on.
620
621 let flags = fcntl(file.as_raw_fd(), FcntlArg::F_GETFL)
622 .into_io_result()?;
623
624 let mut flags = OFlag::from_bits_truncate(flags);
625 flags.remove(OFlag::O_NONBLOCK);
626
627 fcntl(file.as_raw_fd(), FcntlArg::F_SETFL(flags))
628 .into_io_result()?;
629
630 check_tape_is_linux_tape_device(&file)
631 .map_err(|err| format_err!("device type check {:?} failed - {}", path, err))?;
632
633 Ok(file)
634 }
635
636 /// like BlockedWriter, but writes EOF mark on finish
637 pub struct TapeWriterHandle<'a> {
638 writer: BlockedWriter<&'a mut File>,
639 }
640
641 impl TapeWrite for TapeWriterHandle<'_> {
642
643 fn write_all(&mut self, data: &[u8]) -> Result<bool, std::io::Error> {
644 self.writer.write_all(data)
645 }
646
647 fn bytes_written(&self) -> usize {
648 self.writer.bytes_written()
649 }
650
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())?;
655 Ok(leof)
656 }
657
658 fn logical_end_of_media(&self) -> bool {
659 self.writer.logical_end_of_media()
660 }
661 }