]>
Commit | Line | Data |
---|---|---|
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 | ||
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}; | |
5d6379f8 | 20 | use std::convert::TryInto; |
a79082a0 DM |
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 | ||
b2065dc7 WB |
30 | use pbs_api_types::Fingerprint; |
31 | use pbs_datastore::key_derivation::KeyConfig; | |
32 | ||
a79082a0 DM |
33 | use 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 | ||
57 | impl 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 | |
91 | pub struct LtoTapeHandle { | |
92 | sg_tape: SgTape, | |
93 | } | |
94 | ||
95 | impl 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 | ||
253 | impl 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 | |
425 | pub 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. | |
450 | pub 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 | ||
477 | fn 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 | } |