]> git.proxmox.com Git - proxmox-backup.git/blob - src/tape/drive/lto/mod.rs
tape: add vendor, product and revision to LtoDriveAndMediaStatus
[proxmox-backup.git] / src / tape / drive / lto / mod.rs
1 //! Driver for LTO SCSI tapes
2 //!
3 //! This is a userspace drive implementation using SG_IO.
4 //!
5 //! Why we do not use the Linux tape driver:
6 //!
7 //! - missing features (MAM, Encryption, ...)
8 //!
9 //! - strange permission handling - only root (or CAP_SYS_RAWIO) can
10 //! do SG_IO (SYS_RAW_IO)
11 //!
12 //! - unability to detect EOT (you just get EIO)
13
14 mod sg_tape;
15 pub use sg_tape::*;
16
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;
21
22 use anyhow::{bail, format_err, Error};
23 use nix::fcntl::{fcntl, FcntlArg, OFlag};
24
25 use proxmox::{
26 tools::Uuid,
27 sys::error::SysResult,
28 };
29
30 use crate::{
31 config,
32 tools::run_command,
33 backup::{
34 Fingerprint,
35 KeyConfig,
36 },
37 api2::types::{
38 MamAttribute,
39 LtoDriveAndMediaStatus,
40 LtoTapeDrive,
41 },
42 tape::{
43 TapeRead,
44 TapeWrite,
45 drive::{
46 TapeDriver,
47 TapeAlertFlags,
48 Lp17VolumeStatistics,
49 mam_extract_media_usage,
50 },
51 file_formats::{
52 PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0,
53 MediaSetLabel,
54 MediaContentHeader,
55 },
56 },
57 };
58
59 impl LtoTapeDrive {
60
61 /// Open a tape device
62 ///
63 /// This does additional checks:
64 ///
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> {
70
71 proxmox::try_block!({
72 let file = open_lto_tape_device(&self.path)?;
73
74 let mut handle = LtoTapeHandle::new(file)?;
75
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
80 }
81 }
82
83 handle.sg_tape.wait_until_ready()?;
84
85 handle.set_default_options()?;
86
87 Ok(handle)
88 }).map_err(|err: Error| format_err!("open drive '{}' ({}) failed - {}", self.name, self.path, err))
89 }
90 }
91
92 /// Lto Tape device handle
93 pub struct LtoTapeHandle {
94 sg_tape: SgTape,
95 }
96
97 impl LtoTapeHandle {
98
99 /// Creates a new instance
100 pub fn new(file: File) -> Result<Self, Error> {
101 let sg_tape = SgTape::new(file)?;
102 Ok(Self { sg_tape })
103 }
104
105 /// Set all options we need/want
106 pub fn set_default_options(&mut self) -> Result<(), Error> {
107
108 let compression = Some(true);
109 let block_length = Some(0); // variable length mode
110 let buffer_mode = Some(true); // Always use drive buffer
111
112 self.set_drive_options(compression, block_length, buffer_mode)?;
113
114 Ok(())
115 }
116
117 /// Set driver options
118 pub fn set_drive_options(
119 &mut self,
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)
125 }
126
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)
130 }
131
132 /// Get Tape and Media status
133 pub fn get_drive_and_media_status(&mut self) -> Result<LtoDriveAndMediaStatus, Error> {
134
135 let drive_status = self.sg_tape.read_drive_status()?;
136
137 let alert_flags = self.tape_alert_flags()
138 .map(|flags| format!("{:?}", flags))
139 .ok();
140
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()?,
149 alert_flags,
150 write_protect: None,
151 file_number: None,
152 block_number: None,
153 manufactured: None,
154 bytes_read: None,
155 bytes_written: None,
156 medium_passes: None,
157 medium_wearout: None,
158 volume_mounts: None,
159 };
160
161 if self.sg_tape.test_unit_ready().is_ok() {
162
163 if drive_status.write_protect {
164 status.write_protect = Some(drive_status.write_protect);
165 }
166
167 let position = self.sg_tape.position()?;
168
169 status.file_number = Some(position.logical_file_id);
170 status.block_number = Some(position.logical_object_number);
171
172 if let Ok(mam) = self.cartridge_memory() {
173
174 let usage = mam_extract_media_usage(&mam)?;
175
176 status.manufactured = Some(usage.manufactured);
177 status.bytes_read = Some(usage.bytes_read);
178 status.bytes_written = Some(usage.bytes_written);
179
180 if let Ok(volume_stats) = self.volume_statistics() {
181
182 let passes = std::cmp::max(
183 volume_stats.beginning_of_medium_passes,
184 volume_stats.middle_of_tape_passes,
185 );
186
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);
190
191 status.medium_passes = Some(passes);
192 status.medium_wearout = Some(wearout);
193
194 status.volume_mounts = Some(volume_stats.volume_mounts);
195 }
196 }
197 }
198
199 Ok(status)
200 }
201
202 pub fn forward_space_count_files(&mut self, count: usize) -> Result<(), Error> {
203 self.sg_tape.space_filemarks(count.try_into()?)
204 }
205
206 pub fn backward_space_count_files(&mut self, count: usize) -> Result<(), Error> {
207 self.sg_tape.space_filemarks(-count.try_into()?)
208 }
209
210 pub fn forward_space_count_records(&mut self, count: usize) -> Result<(), Error> {
211 self.sg_tape.space_blocks(count.try_into()?)
212 }
213
214 pub fn backward_space_count_records(&mut self, count: usize) -> Result<(), Error> {
215 self.sg_tape.space_blocks(-count.try_into()?)
216 }
217
218 /// Position the tape after filemark count. Count 0 means BOT.
219 ///
220 /// Note: we dont use LOCATE(10), because that needs LTO5
221 pub fn locate_file(&mut self, position: u64) -> Result<(), Error> {
222
223 if position == 0 {
224 return self.rewind();
225 }
226
227 let current_position = self.current_file_number()?;
228
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()?)?;
236 } else {
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)?;
241 }
242
243 Ok(())
244 }
245
246 pub fn erase_media(&mut self, fast: bool) -> Result<(), Error> {
247 self.sg_tape.erase_media(fast)
248 }
249
250 pub fn load(&mut self) -> Result<(), Error> {
251 self.sg_tape.load()
252 }
253
254 /// Read Cartridge Memory (MAM Attributes)
255 pub fn cartridge_memory(&mut self) -> Result<Vec<MamAttribute>, Error> {
256 self.sg_tape.cartridge_memory()
257 }
258
259 /// Read Volume Statistics
260 pub fn volume_statistics(&mut self) -> Result<Lp17VolumeStatistics, Error> {
261 self.sg_tape.volume_statistics()
262 }
263
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))
268 }
269
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))
274 }
275 }
276
277
278 impl TapeDriver for LtoTapeHandle {
279
280 fn sync(&mut self) -> Result<(), Error> {
281 self.sg_tape.sync()?;
282 Ok(())
283 }
284
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)
288 }
289
290 fn move_to_last_file(&mut self) -> Result<(), Error> {
291
292 self.move_to_eom(false)?;
293
294 self.sg_tape.check_filemark()?;
295
296 let pos = self.current_file_number()?;
297
298 if pos == 0 {
299 bail!("move_to_last_file failed - media contains no data");
300 }
301
302 if pos == 1 {
303 self.rewind()?;
304 return Ok(());
305 }
306
307 self.backward_space_count_files(2)?;
308 self.forward_space_count_files(1)?;
309
310 Ok(())
311 }
312
313 fn rewind(&mut self) -> Result<(), Error> {
314 self.sg_tape.rewind()
315 }
316
317 fn current_file_number(&mut self) -> Result<u64, Error> {
318 self.sg_tape.current_file_number()
319 }
320
321 fn format_media(&mut self, fast: bool) -> Result<(), Error> {
322 self.sg_tape.format_media(fast)
323 }
324
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 {
328 Some(reader) => {
329 let reader: Box<dyn TapeRead> = Box::new(reader);
330 Some(reader)
331 }
332 None => None,
333 };
334
335 Ok(handle)
336 }
337
338 fn write_file<'a>(&'a mut self) -> Result<Box<dyn TapeWrite + 'a>, std::io::Error> {
339 let handle = self.sg_tape.open_writer();
340 Ok(Box::new(handle))
341 }
342
343 fn write_media_set_label(
344 &mut self,
345 media_set_label: &MediaSetLabel,
346 key_config: Option<&KeyConfig>,
347 ) -> Result<(), Error> {
348
349 let file_number = self.current_file_number()?;
350 if file_number != 1 {
351 self.rewind()?;
352 self.forward_space_count_files(1)?; // skip label
353 }
354
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);
358 }
359
360 self.set_encryption(None)?;
361
362 { // limit handle scope
363 let mut handle = self.write_file()?;
364
365 let mut value = serde_json::to_value(media_set_label)?;
366 if media_set_label.encryption_key_fingerprint.is_some() {
367 match key_config {
368 Some(key_config) => {
369 value["key-config"] = serde_json::to_value(key_config)?;
370 }
371 None => {
372 bail!("missing encryption key config");
373 }
374 }
375 }
376
377 let raw = serde_json::to_string_pretty(&value)?;
378
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)?;
382 }
383
384 self.sync()?; // sync data to tape
385
386 Ok(())
387 }
388
389 /// Rewind and put the drive off line (Eject media).
390 fn eject_media(&mut self) -> Result<(), Error> {
391 self.sg_tape.eject()
392 }
393
394 /// Read Tape Alert Flags
395 fn tape_alert_flags(&mut self) -> Result<TapeAlertFlags, Error> {
396 self.sg_tape.tape_alert_flags()
397 }
398
399 /// Set or clear encryption key
400 ///
401 /// Note: Only 'root' can read secret encryption keys, so we need
402 /// to spawn setuid binary 'sg-tape-cmd'.
403 fn set_encryption(
404 &mut self,
405 key_fingerprint: Option<(Fingerprint, Uuid)>,
406 ) -> Result<(), Error> {
407
408 if nix::unistd::Uid::effective().is_root() {
409
410 if let Some((ref key_fingerprint, ref uuid)) = key_fingerprint {
411
412 let (key_map, _digest) = config::tape_encryption_keys::load_keys()?;
413 match key_map.get(key_fingerprint) {
414 Some(item) => {
415
416 // derive specialized key for each media-set
417
418 let mut tape_key = [0u8; 32];
419
420 let uuid_bytes: [u8; 16] = uuid.as_bytes().clone();
421
422 openssl::pkcs5::pbkdf2_hmac(
423 &item.key,
424 &uuid_bytes,
425 10,
426 openssl::hash::MessageDigest::sha256(),
427 &mut tape_key)?;
428
429 return self.sg_tape.set_encryption(Some(tape_key));
430 }
431 None => bail!("unknown tape encryption key '{}'", key_fingerprint),
432 }
433 } else {
434 return self.sg_tape.set_encryption(None);
435 }
436 }
437
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())?
444 } else {
445 run_sg_tape_cmd("encryption", &[], self.sg_tape.file_mut().as_raw_fd())?
446 };
447 let result: Result<(), String> = serde_json::from_str(&output)?;
448 result.map_err(|err| format_err!("{}", err))
449 }
450 }
451
452 /// Check for correct Major/Minor numbers
453 pub fn check_tape_is_lto_tape_device(file: &File) -> Result<(), Error> {
454
455 let stat = nix::sys::stat::fstat(file.as_raw_fd())?;
456
457 let devnum = stat.st_rdev;
458
459 let major = unsafe { libc::major(devnum) };
460 let _minor = unsafe { libc::minor(devnum) };
461
462 if major == 9 {
463 bail!("not a scsi-generic tape device (cannot use linux tape devices)");
464 }
465
466 if major != 21 {
467 bail!("not a scsi-generic tape device");
468 }
469
470 Ok(())
471 }
472
473 /// Opens a Lto tape device
474 ///
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
477 /// device.
478 pub fn open_lto_tape_device(
479 path: &str,
480 ) -> Result<File, Error> {
481
482 let file = OpenOptions::new()
483 .read(true)
484 .write(true)
485 .custom_flags(libc::O_NONBLOCK)
486 .open(path)?;
487
488 // clear O_NONBLOCK from now on.
489
490 let flags = fcntl(file.as_raw_fd(), FcntlArg::F_GETFL)
491 .into_io_result()?;
492
493 let mut flags = OFlag::from_bits_truncate(flags);
494 flags.remove(OFlag::O_NONBLOCK);
495
496 fcntl(file.as_raw_fd(), FcntlArg::F_SETFL(flags))
497 .into_io_result()?;
498
499 check_tape_is_lto_tape_device(&file)
500 .map_err(|err| format_err!("device type check {:?} failed - {}", path, err))?;
501
502 Ok(file)
503 }
504
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"]);
510 command.args(args);
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)
514 }