]> git.proxmox.com Git - proxmox-backup.git/blame - src/tape/drive/lto/mod.rs
fixup imports in tests and examples
[proxmox-backup.git] / src / tape / drive / lto / mod.rs
CommitLineData
a79082a0
DM
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
14mod sg_tape;
15pub use sg_tape::*;
16
17use std::fs::{OpenOptions, File};
18use std::os::unix::fs::OpenOptionsExt;
19use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
5d6379f8 20use std::convert::TryInto;
a79082a0
DM
21
22use anyhow::{bail, format_err, Error};
23use nix::fcntl::{fcntl, FcntlArg, OFlag};
24
25use proxmox::{
26 tools::Uuid,
27 sys::error::SysResult,
28};
29
b2065dc7
WB
30use pbs_api_types::Fingerprint;
31use pbs_datastore::key_derivation::KeyConfig;
32
a79082a0
DM
33use crate::{
34 config,
35 tools::run_command,
a79082a0
DM
36 api2::types::{
37 MamAttribute,
38 LtoDriveAndMediaStatus,
39 LtoTapeDrive,
109ccd30 40 Lp17VolumeStatistics,
a79082a0
DM
41 },
42 tape::{
43 TapeRead,
44 TapeWrite,
318b3106 45 BlockReadError,
a79082a0
DM
46 drive::{
47 TapeDriver,
a79082a0
DM
48 },
49 file_formats::{
50 PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0,
51 MediaSetLabel,
52 MediaContentHeader,
53 },
54 },
55};
56
57impl LtoTapeDrive {
58
59 /// Open a tape device
60 ///
61 /// This does additional checks:
62 ///
63 /// - check if it is a non-rewinding tape device
64 /// - check if drive is ready (tape loaded)
65 /// - check block size
66 /// - for autoloader only, try to reload ejected tapes
67 pub fn open(&self) -> Result<LtoTapeHandle, Error> {
68
69 proxmox::try_block!({
70 let file = open_lto_tape_device(&self.path)?;
71
72 let mut handle = LtoTapeHandle::new(file)?;
73
74 if !handle.sg_tape.test_unit_ready().is_ok() {
75 // for autoloader only, try to reload ejected tapes
76 if self.changer.is_some() {
77 let _ = handle.sg_tape.load(); // just try, ignore error
78 }
79 }
80
81 handle.sg_tape.wait_until_ready()?;
82
0892a512 83 handle.set_default_options()?;
a79082a0
DM
84
85 Ok(handle)
86 }).map_err(|err: Error| format_err!("open drive '{}' ({}) failed - {}", self.name, self.path, err))
87 }
88}
89
90/// Lto Tape device handle
91pub struct LtoTapeHandle {
92 sg_tape: SgTape,
93}
94
95impl LtoTapeHandle {
96
97 /// Creates a new instance
98 pub fn new(file: File) -> Result<Self, Error> {
99 let sg_tape = SgTape::new(file)?;
100 Ok(Self { sg_tape })
101 }
102
103 /// Set all options we need/want
0892a512
DM
104 pub fn set_default_options(&mut self) -> Result<(), Error> {
105
106 let compression = Some(true);
107 let block_length = Some(0); // variable length mode
108 let buffer_mode = Some(true); // Always use drive buffer
109
80ea23e1 110 self.set_drive_options(compression, block_length, buffer_mode)?;
0892a512 111
a79082a0
DM
112 Ok(())
113 }
114
80ea23e1
DM
115 /// Set driver options
116 pub fn set_drive_options(
117 &mut self,
118 compression: Option<bool>,
119 block_length: Option<u32>,
120 buffer_mode: Option<bool>,
121 ) -> Result<(), Error> {
122 self.sg_tape.set_drive_options(compression, block_length, buffer_mode)
123 }
124
a79082a0
DM
125 /// Write a single EOF mark without flushing buffers
126 pub fn write_filemarks(&mut self, count: usize) -> Result<(), std::io::Error> {
127 self.sg_tape.write_filemarks(count, false)
128 }
129
130 /// Get Tape and Media status
131 pub fn get_drive_and_media_status(&mut self) -> Result<LtoDriveAndMediaStatus, Error> {
132
0892a512 133 let drive_status = self.sg_tape.read_drive_status()?;
a79082a0
DM
134
135 let alert_flags = self.tape_alert_flags()
136 .map(|flags| format!("{:?}", flags))
137 .ok();
138
139 let mut status = LtoDriveAndMediaStatus {
15d14357
DM
140 vendor: self.sg_tape.info().vendor.clone(),
141 product: self.sg_tape.info().product.clone(),
142 revision: self.sg_tape.info().revision.clone(),
0892a512
DM
143 blocksize: drive_status.block_length,
144 compression: drive_status.compression,
145 buffer_mode: drive_status.buffer_mode,
5d6379f8 146 density: drive_status.density_code.try_into()?,
a79082a0 147 alert_flags,
0892a512
DM
148 write_protect: None,
149 file_number: None,
150 block_number: None,
a79082a0
DM
151 manufactured: None,
152 bytes_read: None,
153 bytes_written: None,
154 medium_passes: None,
155 medium_wearout: None,
156 volume_mounts: None,
157 };
158
0892a512
DM
159 if self.sg_tape.test_unit_ready().is_ok() {
160
161 if drive_status.write_protect {
162 status.write_protect = Some(drive_status.write_protect);
163 }
164
165 let position = self.sg_tape.position()?;
166
167 status.file_number = Some(position.logical_file_id);
168 status.block_number = Some(position.logical_object_number);
a79082a0
DM
169
170 if let Ok(mam) = self.cartridge_memory() {
171
172 let usage = mam_extract_media_usage(&mam)?;
173
174 status.manufactured = Some(usage.manufactured);
175 status.bytes_read = Some(usage.bytes_read);
176 status.bytes_written = Some(usage.bytes_written);
177
178 if let Ok(volume_stats) = self.volume_statistics() {
179
180 let passes = std::cmp::max(
181 volume_stats.beginning_of_medium_passes,
182 volume_stats.middle_of_tape_passes,
183 );
184
185 // assume max. 16000 medium passes
186 // see: https://en.wikipedia.org/wiki/Linear_Tape-Open
187 let wearout: f64 = (passes as f64)/(16000.0 as f64);
188
189 status.medium_passes = Some(passes);
190 status.medium_wearout = Some(wearout);
191
192 status.volume_mounts = Some(volume_stats.volume_mounts);
193 }
194 }
195 }
196
197 Ok(status)
198 }
199
8b2c6f5d 200 pub fn forward_space_count_files(&mut self, count: usize) -> Result<(), Error> {
5d6379f8 201 self.sg_tape.space_filemarks(count.try_into()?)
8b2c6f5d
DM
202 }
203
204 pub fn backward_space_count_files(&mut self, count: usize) -> Result<(), Error> {
5d6379f8 205 self.sg_tape.space_filemarks(-count.try_into()?)
8b2c6f5d
DM
206 }
207
7f745967 208 pub fn forward_space_count_records(&mut self, count: usize) -> Result<(), Error> {
5d6379f8 209 self.sg_tape.space_blocks(count.try_into()?)
7f745967
DM
210 }
211
212 pub fn backward_space_count_records(&mut self, count: usize) -> Result<(), Error> {
5d6379f8
DM
213 self.sg_tape.space_blocks(-count.try_into()?)
214 }
215
216 /// Position the tape after filemark count. Count 0 means BOT.
5d6379f8 217 pub fn locate_file(&mut self, position: u64) -> Result<(), Error> {
bbbf662d 218 self.sg_tape.locate_file(position)
7f745967
DM
219 }
220
e29f456e
DM
221 pub fn erase_media(&mut self, fast: bool) -> Result<(), Error> {
222 self.sg_tape.erase_media(fast)
223 }
224
a79082a0
DM
225 pub fn load(&mut self) -> Result<(), Error> {
226 self.sg_tape.load()
227 }
228
229 /// Read Cartridge Memory (MAM Attributes)
230 pub fn cartridge_memory(&mut self) -> Result<Vec<MamAttribute>, Error> {
231 self.sg_tape.cartridge_memory()
232 }
233
234 /// Read Volume Statistics
235 pub fn volume_statistics(&mut self) -> Result<Lp17VolumeStatistics, Error> {
236 self.sg_tape.volume_statistics()
237 }
566b946f
DM
238
239 /// Lock the drive door
240 pub fn lock(&mut self) -> Result<(), Error> {
241 self.sg_tape.set_medium_removal(false)
242 .map_err(|err| format_err!("lock door failed - {}", err))
243 }
244
245 /// Unlock the drive door
246 pub fn unlock(&mut self) -> Result<(), Error> {
247 self.sg_tape.set_medium_removal(true)
248 .map_err(|err| format_err!("unlock door failed - {}", err))
249 }
a79082a0
DM
250}
251
252
253impl TapeDriver for LtoTapeHandle {
254
255 fn sync(&mut self) -> Result<(), Error> {
256 self.sg_tape.sync()?;
257 Ok(())
258 }
259
260 /// Go to the end of the recorded media (for appending files).
7b11a809
DM
261 fn move_to_eom(&mut self, write_missing_eof: bool) -> Result<(), Error> {
262 self.sg_tape.move_to_eom(write_missing_eof)
a79082a0
DM
263 }
264
8b2c6f5d 265 fn move_to_last_file(&mut self) -> Result<(), Error> {
a79082a0 266
7b11a809
DM
267 self.move_to_eom(false)?;
268
269 self.sg_tape.check_filemark()?;
8b2c6f5d
DM
270
271 let pos = self.current_file_number()?;
272
273 if pos == 0 {
274 bail!("move_to_last_file failed - media contains no data");
275 }
276
277 if pos == 1 {
278 self.rewind()?;
279 return Ok(());
280 }
281
282 self.backward_space_count_files(2)?;
283 self.forward_space_count_files(1)?;
284
285 Ok(())
a79082a0
DM
286 }
287
56d36ca4
DC
288 fn move_to_file(&mut self, file: u64) -> Result<(), Error> {
289 self.locate_file(file)
290 }
291
a79082a0
DM
292 fn rewind(&mut self) -> Result<(), Error> {
293 self.sg_tape.rewind()
294 }
295
296 fn current_file_number(&mut self) -> Result<u64, Error> {
297 self.sg_tape.current_file_number()
298 }
299
e29f456e
DM
300 fn format_media(&mut self, fast: bool) -> Result<(), Error> {
301 self.sg_tape.format_media(fast)
a79082a0
DM
302 }
303
318b3106 304 fn read_next_file<'a>(&'a mut self) -> Result<Box<dyn TapeRead + 'a>, BlockReadError> {
a79082a0 305 let reader = self.sg_tape.open_reader()?;
318b3106 306 let handle: Box<dyn TapeRead> = Box::new(reader);
a79082a0
DM
307 Ok(handle)
308 }
309
310 fn write_file<'a>(&'a mut self) -> Result<Box<dyn TapeWrite + 'a>, std::io::Error> {
311 let handle = self.sg_tape.open_writer();
312 Ok(Box::new(handle))
313 }
314
315 fn write_media_set_label(
316 &mut self,
317 media_set_label: &MediaSetLabel,
318 key_config: Option<&KeyConfig>,
319 ) -> Result<(), Error> {
320
321 let file_number = self.current_file_number()?;
322 if file_number != 1 {
323 self.rewind()?;
324 self.forward_space_count_files(1)?; // skip label
325 }
326
327 let file_number = self.current_file_number()?;
328 if file_number != 1 {
329 bail!("write_media_set_label failed - got wrong file number ({} != 1)", file_number);
330 }
331
332 self.set_encryption(None)?;
333
334 { // limit handle scope
335 let mut handle = self.write_file()?;
336
337 let mut value = serde_json::to_value(media_set_label)?;
338 if media_set_label.encryption_key_fingerprint.is_some() {
339 match key_config {
340 Some(key_config) => {
341 value["key-config"] = serde_json::to_value(key_config)?;
342 }
343 None => {
344 bail!("missing encryption key config");
345 }
346 }
347 }
348
349 let raw = serde_json::to_string_pretty(&value)?;
350
351 let header = MediaContentHeader::new(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, raw.len() as u32);
352 handle.write_header(&header, raw.as_bytes())?;
353 handle.finish(false)?;
354 }
355
356 self.sync()?; // sync data to tape
357
358 Ok(())
359 }
360
361 /// Rewind and put the drive off line (Eject media).
362 fn eject_media(&mut self) -> Result<(), Error> {
363 self.sg_tape.eject()
364 }
365
366 /// Read Tape Alert Flags
367 fn tape_alert_flags(&mut self) -> Result<TapeAlertFlags, Error> {
368 self.sg_tape.tape_alert_flags()
369 }
370
371 /// Set or clear encryption key
372 ///
373 /// Note: Only 'root' can read secret encryption keys, so we need
374 /// to spawn setuid binary 'sg-tape-cmd'.
375 fn set_encryption(
376 &mut self,
377 key_fingerprint: Option<(Fingerprint, Uuid)>,
378 ) -> Result<(), Error> {
379
380 if nix::unistd::Uid::effective().is_root() {
381
382 if let Some((ref key_fingerprint, ref uuid)) = key_fingerprint {
383
384 let (key_map, _digest) = config::tape_encryption_keys::load_keys()?;
385 match key_map.get(key_fingerprint) {
386 Some(item) => {
387
388 // derive specialized key for each media-set
389
390 let mut tape_key = [0u8; 32];
391
392 let uuid_bytes: [u8; 16] = uuid.as_bytes().clone();
393
394 openssl::pkcs5::pbkdf2_hmac(
395 &item.key,
396 &uuid_bytes,
397 10,
398 openssl::hash::MessageDigest::sha256(),
399 &mut tape_key)?;
400
401 return self.sg_tape.set_encryption(Some(tape_key));
402 }
403 None => bail!("unknown tape encryption key '{}'", key_fingerprint),
404 }
405 } else {
406 return self.sg_tape.set_encryption(None);
407 }
408 }
409
410 let output = if let Some((fingerprint, uuid)) = key_fingerprint {
770a36e5 411 let fingerprint = pbs_tools::format::as_fingerprint(fingerprint.bytes());
a79082a0
DM
412 run_sg_tape_cmd("encryption", &[
413 "--fingerprint", &fingerprint,
414 "--uuid", &uuid.to_string(),
415 ], self.sg_tape.file_mut().as_raw_fd())?
416 } else {
417 run_sg_tape_cmd("encryption", &[], self.sg_tape.file_mut().as_raw_fd())?
418 };
419 let result: Result<(), String> = serde_json::from_str(&output)?;
420 result.map_err(|err| format_err!("{}", err))
421 }
422}
423
424/// Check for correct Major/Minor numbers
425pub fn check_tape_is_lto_tape_device(file: &File) -> Result<(), Error> {
426
427 let stat = nix::sys::stat::fstat(file.as_raw_fd())?;
428
429 let devnum = stat.st_rdev;
430
431 let major = unsafe { libc::major(devnum) };
432 let _minor = unsafe { libc::minor(devnum) };
433
434 if major == 9 {
435 bail!("not a scsi-generic tape device (cannot use linux tape devices)");
436 }
437
438 if major != 21 {
439 bail!("not a scsi-generic tape device");
440 }
441
442 Ok(())
443}
444
445/// Opens a Lto tape device
446///
447/// The open call use O_NONBLOCK, but that flag is cleard after open
448/// succeeded. This also checks if the device is a non-rewinding tape
449/// device.
450pub fn open_lto_tape_device(
451 path: &str,
452) -> Result<File, Error> {
453
454 let file = OpenOptions::new()
455 .read(true)
456 .write(true)
457 .custom_flags(libc::O_NONBLOCK)
458 .open(path)?;
459
460 // clear O_NONBLOCK from now on.
461
462 let flags = fcntl(file.as_raw_fd(), FcntlArg::F_GETFL)
463 .into_io_result()?;
464
465 let mut flags = OFlag::from_bits_truncate(flags);
466 flags.remove(OFlag::O_NONBLOCK);
467
468 fcntl(file.as_raw_fd(), FcntlArg::F_SETFL(flags))
469 .into_io_result()?;
470
471 check_tape_is_lto_tape_device(&file)
472 .map_err(|err| format_err!("device type check {:?} failed - {}", path, err))?;
473
474 Ok(file)
475}
476
477fn run_sg_tape_cmd(subcmd: &str, args: &[&str], fd: RawFd) -> Result<String, Error> {
478 let mut command = std::process::Command::new(
479 "/usr/lib/x86_64-linux-gnu/proxmox-backup/sg-tape-cmd");
480 command.args(&[subcmd]);
481 command.args(&["--stdin"]);
482 command.args(args);
483 let device_fd = nix::unistd::dup(fd)?;
484 command.stdin(unsafe { std::process::Stdio::from_raw_fd(device_fd)});
485 run_command(command, None)
486}