]> git.proxmox.com Git - proxmox-backup.git/blob - src/tape/drive/lto/mod.rs
fix #4904: tape changer: add option to eject before unload
[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 use std::fs::File;
15 use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
16
17 use anyhow::{bail, format_err, Error};
18
19 use proxmox_uuid::Uuid;
20
21 use pbs_api_types::{
22 Fingerprint, Lp17VolumeStatistics, LtoDriveAndMediaStatus, LtoTapeDrive, MamAttribute,
23 };
24 use pbs_key_config::KeyConfig;
25 use pbs_tape::{
26 linux_list_drives::open_lto_tape_device,
27 sg_tape::{SgTape, TapeAlertFlags},
28 BlockReadError, MediaContentHeader, TapeRead, TapeWrite,
29 };
30 use proxmox_sys::command::run_command;
31
32 use crate::tape::{
33 drive::TapeDriver,
34 file_formats::{MediaSetLabel, PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0},
35 };
36
37 /// Open a tape device
38 ///
39 /// This does additional checks:
40 ///
41 /// - check if it is a non-rewinding tape device
42 /// - check if drive is ready (tape loaded)
43 /// - check block size
44 /// - for autoloader only, try to reload ejected tapes
45 pub fn open_lto_tape_drive(config: &LtoTapeDrive) -> Result<LtoTapeHandle, Error> {
46 proxmox_lang::try_block!({
47 let file = open_lto_tape_device(&config.path)?;
48
49 let mut handle = LtoTapeHandle::new(file)?;
50
51 if handle.sg_tape.test_unit_ready().is_err() {
52 // for autoloader only, try to reload ejected tapes
53 if config.changer.is_some() {
54 let _ = handle.sg_tape.load(); // just try, ignore error
55 }
56 }
57
58 handle.sg_tape.wait_until_ready(None)?;
59
60 handle.set_default_options()?;
61
62 Ok(handle)
63 })
64 .map_err(|err: Error| {
65 format_err!(
66 "open drive '{}' ({}) failed - {}",
67 config.name,
68 config.path,
69 err
70 )
71 })
72 }
73
74 /// Lto Tape device handle
75 pub struct LtoTapeHandle {
76 sg_tape: SgTape,
77 }
78
79 impl LtoTapeHandle {
80 /// Creates a new instance
81 pub fn new(file: File) -> Result<Self, Error> {
82 let sg_tape = SgTape::new(file)?;
83 Ok(Self { sg_tape })
84 }
85
86 /// Set all options we need/want
87 pub fn set_default_options(&mut self) -> Result<(), Error> {
88 self.sg_tape.set_default_options()?;
89 Ok(())
90 }
91
92 /// Set driver options
93 pub fn set_drive_options(
94 &mut self,
95 compression: Option<bool>,
96 block_length: Option<u32>,
97 buffer_mode: Option<bool>,
98 ) -> Result<(), Error> {
99 self.sg_tape
100 .set_drive_options(compression, block_length, buffer_mode)
101 }
102
103 /// Write a single EOF mark without flushing buffers
104 pub fn write_filemarks(&mut self, count: usize) -> Result<(), std::io::Error> {
105 self.sg_tape.write_filemarks(count, false)
106 }
107
108 /// Get Tape and Media status
109 pub fn get_drive_and_media_status(&mut self) -> Result<LtoDriveAndMediaStatus, Error> {
110 self.sg_tape.get_drive_and_media_status()
111 }
112
113 pub fn forward_space_count_files(&mut self, count: usize) -> Result<(), Error> {
114 self.sg_tape.space_filemarks(count.try_into()?)
115 }
116
117 pub fn backward_space_count_files(&mut self, count: usize) -> Result<(), Error> {
118 self.sg_tape.space_filemarks(-count.try_into()?)
119 }
120
121 pub fn forward_space_count_records(&mut self, count: usize) -> Result<(), Error> {
122 self.sg_tape.space_blocks(count.try_into()?)
123 }
124
125 pub fn backward_space_count_records(&mut self, count: usize) -> Result<(), Error> {
126 self.sg_tape.space_blocks(-count.try_into()?)
127 }
128
129 /// Position the tape after filemark count. Count 0 means BOT.
130 pub fn locate_file(&mut self, position: u64) -> Result<(), Error> {
131 self.sg_tape.locate_file(position)
132 }
133
134 pub fn erase_media(&mut self, fast: bool) -> Result<(), Error> {
135 self.sg_tape.erase_media(fast)
136 }
137
138 pub fn load(&mut self) -> Result<(), Error> {
139 self.sg_tape.load()
140 }
141
142 /// Read Cartridge Memory (MAM Attributes)
143 pub fn cartridge_memory(&mut self) -> Result<Vec<MamAttribute>, Error> {
144 self.sg_tape.cartridge_memory()
145 }
146
147 /// Read Volume Statistics
148 pub fn volume_statistics(&mut self) -> Result<Lp17VolumeStatistics, Error> {
149 self.sg_tape.volume_statistics()
150 }
151
152 /// Lock the drive door
153 pub fn lock(&mut self) -> Result<(), Error> {
154 self.sg_tape
155 .set_medium_removal(false)
156 .map_err(|err| format_err!("lock door failed - {}", err))
157 }
158
159 /// Unlock the drive door
160 pub fn unlock(&mut self) -> Result<(), Error> {
161 self.sg_tape
162 .set_medium_removal(true)
163 .map_err(|err| format_err!("unlock door failed - {}", err))
164 }
165
166 /// Returns if a medium is present
167 pub fn medium_present(&mut self) -> bool {
168 self.sg_tape.test_unit_ready().is_ok()
169 }
170 }
171
172 impl TapeDriver for LtoTapeHandle {
173 fn sync(&mut self) -> Result<(), Error> {
174 self.sg_tape.sync()?;
175 Ok(())
176 }
177
178 /// Go to the end of the recorded media (for appending files).
179 fn move_to_eom(&mut self, write_missing_eof: bool) -> Result<(), Error> {
180 self.sg_tape.move_to_eom(write_missing_eof)
181 }
182
183 fn move_to_last_file(&mut self) -> Result<(), Error> {
184 self.move_to_eom(false)?;
185
186 self.sg_tape.check_filemark()?;
187
188 let pos = self.current_file_number()?;
189
190 if pos == 0 {
191 bail!("move_to_last_file failed - media contains no data");
192 }
193
194 if pos == 1 {
195 self.rewind()?;
196 return Ok(());
197 }
198
199 self.backward_space_count_files(2)?;
200 self.forward_space_count_files(1)?;
201
202 Ok(())
203 }
204
205 fn move_to_file(&mut self, file: u64) -> Result<(), Error> {
206 self.locate_file(file)
207 }
208
209 fn rewind(&mut self) -> Result<(), Error> {
210 self.sg_tape.rewind()
211 }
212
213 fn current_file_number(&mut self) -> Result<u64, Error> {
214 self.sg_tape.current_file_number()
215 }
216
217 fn format_media(&mut self, fast: bool) -> Result<(), Error> {
218 self.sg_tape.format_media(fast)
219 }
220
221 fn read_next_file<'a>(&'a mut self) -> Result<Box<dyn TapeRead + 'a>, BlockReadError> {
222 let reader = self.sg_tape.open_reader()?;
223 let handle: Box<dyn TapeRead> = Box::new(reader);
224 Ok(handle)
225 }
226
227 fn write_file<'a>(&'a mut self) -> Result<Box<dyn TapeWrite + 'a>, std::io::Error> {
228 let handle = self.sg_tape.open_writer();
229 Ok(Box::new(handle))
230 }
231
232 fn write_media_set_label(
233 &mut self,
234 media_set_label: &MediaSetLabel,
235 key_config: Option<&KeyConfig>,
236 ) -> Result<(), Error> {
237 let file_number = self.current_file_number()?;
238 if file_number != 1 {
239 self.rewind()?;
240 self.forward_space_count_files(1)?; // skip label
241 }
242
243 let file_number = self.current_file_number()?;
244 if file_number != 1 {
245 bail!(
246 "write_media_set_label failed - got wrong file number ({} != 1)",
247 file_number
248 );
249 }
250
251 self.set_encryption(None)?;
252
253 {
254 // limit handle scope
255 let mut handle = self.write_file()?;
256
257 let mut value = serde_json::to_value(media_set_label)?;
258 if media_set_label.encryption_key_fingerprint.is_some() {
259 match key_config {
260 Some(key_config) => {
261 value["key-config"] = serde_json::to_value(key_config)?;
262 }
263 None => {
264 bail!("missing encryption key config");
265 }
266 }
267 }
268
269 let raw = serde_json::to_string_pretty(&value)?;
270
271 let header =
272 MediaContentHeader::new(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, raw.len() as u32);
273 handle.write_header(&header, raw.as_bytes())?;
274 handle.finish(false)?;
275 }
276
277 self.sync()?; // sync data to tape
278
279 Ok(())
280 }
281
282 /// Rewind and put the drive off line (Eject media).
283 fn eject_media(&mut self) -> Result<(), Error> {
284 self.sg_tape.eject()
285 }
286
287 /// Read Tape Alert Flags
288 fn tape_alert_flags(&mut self) -> Result<TapeAlertFlags, Error> {
289 self.sg_tape.tape_alert_flags()
290 }
291
292 /// Set or clear encryption key
293 ///
294 /// Note: Only 'root' can read secret encryption keys, so we need
295 /// to spawn setuid binary 'sg-tape-cmd'.
296 fn set_encryption(
297 &mut self,
298 key_fingerprint: Option<(Fingerprint, Uuid)>,
299 ) -> Result<(), Error> {
300 if nix::unistd::Uid::effective().is_root() {
301 if let Some((ref key_fingerprint, ref uuid)) = key_fingerprint {
302 let (key_map, _digest) = crate::tape::encryption_keys::load_keys()?;
303 match key_map.get(key_fingerprint) {
304 Some(item) => {
305 // derive specialized key for each media-set
306
307 let mut tape_key = [0u8; 32];
308
309 let uuid_bytes: [u8; 16] = *uuid.as_bytes();
310
311 openssl::pkcs5::pbkdf2_hmac(
312 &item.key,
313 &uuid_bytes,
314 10,
315 openssl::hash::MessageDigest::sha256(),
316 &mut tape_key,
317 )?;
318
319 return self.sg_tape.set_encryption(Some(tape_key));
320 }
321 None => bail!("unknown tape encryption key '{}'", key_fingerprint),
322 }
323 } else {
324 return self.sg_tape.set_encryption(None);
325 }
326 }
327
328 let output = if let Some((fingerprint, uuid)) = key_fingerprint {
329 let fingerprint = fingerprint.signature();
330 run_sg_tape_cmd(
331 "encryption",
332 &["--fingerprint", &fingerprint, "--uuid", &uuid.to_string()],
333 self.sg_tape.file_mut().as_raw_fd(),
334 )?
335 } else {
336 run_sg_tape_cmd("encryption", &[], self.sg_tape.file_mut().as_raw_fd())?
337 };
338 let result: Result<(), String> = serde_json::from_str(&output)?;
339 result.map_err(|err| format_err!("{}", err))
340 }
341 }
342
343 fn run_sg_tape_cmd(subcmd: &str, args: &[&str], fd: RawFd) -> Result<String, Error> {
344 let mut command =
345 std::process::Command::new("/usr/lib/x86_64-linux-gnu/proxmox-backup/sg-tape-cmd");
346 command.args([subcmd]);
347 command.args(["--stdin"]);
348 command.args(args);
349 let device_fd = nix::unistd::dup(fd)?;
350 command.stdin(unsafe { std::process::Stdio::from_raw_fd(device_fd) });
351 run_command(command, None)
352 }