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