]>
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 | ||
048b43af | 14 | use std::fs::File; |
a79082a0 | 15 | use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; |
a79082a0 DM |
16 | |
17 | use anyhow::{bail, format_err, Error}; | |
a79082a0 | 18 | |
6ef1b649 | 19 | use proxmox_uuid::Uuid; |
a79082a0 | 20 | |
5839c469 | 21 | use pbs_api_types::{ |
4de1c42c | 22 | Fingerprint, Lp17VolumeStatistics, LtoDriveAndMediaStatus, LtoTapeDrive, MamAttribute, |
5839c469 | 23 | }; |
1104d2a2 | 24 | use pbs_key_config::KeyConfig; |
048b43af | 25 | use pbs_tape::{ |
4de1c42c TL |
26 | sg_tape::{SgTape, TapeAlertFlags}, |
27 | BlockReadError, MediaContentHeader, TapeRead, TapeWrite, | |
048b43af | 28 | }; |
4de1c42c | 29 | use proxmox_sys::command::run_command; |
b2065dc7 | 30 | |
4de1c42c TL |
31 | use crate::tape::{ |
32 | drive::TapeDriver, | |
33 | file_formats::{MediaSetLabel, PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0}, | |
a79082a0 DM |
34 | }; |
35 | ||
1dc0df33 DC |
36 | impl Drop for LtoTapeHandle { |
37 | fn drop(&mut self) { | |
38 | // always unload the encryption key when the handle is dropped for security | |
39 | // but only log an error if we set one in the first place | |
40 | if let Err(err) = self.set_encryption(None) { | |
41 | if self.encryption_key_loaded { | |
42 | log::error!("could not unload encryption key from drive: {err}"); | |
43 | } | |
44 | } | |
45 | } | |
46 | } | |
47 | ||
a79082a0 DM |
48 | /// Lto Tape device handle |
49 | pub struct LtoTapeHandle { | |
50 | sg_tape: SgTape, | |
1dc0df33 | 51 | encryption_key_loaded: bool, |
a79082a0 DM |
52 | } |
53 | ||
54 | impl LtoTapeHandle { | |
a79082a0 DM |
55 | /// Creates a new instance |
56 | pub fn new(file: File) -> Result<Self, Error> { | |
57 | let sg_tape = SgTape::new(file)?; | |
1dc0df33 DC |
58 | Ok(Self { |
59 | sg_tape, | |
60 | encryption_key_loaded: false, | |
61 | }) | |
a79082a0 DM |
62 | } |
63 | ||
b5f86267 DC |
64 | /// Open a tape device |
65 | /// | |
66 | /// since this calls [SgTape::open_lto_drive], it does some internal checks. | |
67 | /// See [SgTape] docs for details. | |
68 | pub fn open_lto_drive(config: &LtoTapeDrive) -> Result<Self, Error> { | |
69 | let sg_tape = SgTape::open_lto_drive(config)?; | |
70 | ||
1dc0df33 DC |
71 | let handle = Self { |
72 | sg_tape, | |
73 | encryption_key_loaded: false, | |
74 | }; | |
b5f86267 DC |
75 | |
76 | Ok(handle) | |
77 | } | |
78 | ||
a79082a0 | 79 | /// Set all options we need/want |
0892a512 | 80 | pub fn set_default_options(&mut self) -> Result<(), Error> { |
048b43af | 81 | self.sg_tape.set_default_options()?; |
a79082a0 DM |
82 | Ok(()) |
83 | } | |
84 | ||
80ea23e1 DM |
85 | /// Set driver options |
86 | pub fn set_drive_options( | |
87 | &mut self, | |
88 | compression: Option<bool>, | |
89 | block_length: Option<u32>, | |
90 | buffer_mode: Option<bool>, | |
91 | ) -> Result<(), Error> { | |
4de1c42c TL |
92 | self.sg_tape |
93 | .set_drive_options(compression, block_length, buffer_mode) | |
80ea23e1 DM |
94 | } |
95 | ||
a79082a0 DM |
96 | /// Write a single EOF mark without flushing buffers |
97 | pub fn write_filemarks(&mut self, count: usize) -> Result<(), std::io::Error> { | |
98 | self.sg_tape.write_filemarks(count, false) | |
99 | } | |
100 | ||
101 | /// Get Tape and Media status | |
4de1c42c | 102 | pub fn get_drive_and_media_status(&mut self) -> Result<LtoDriveAndMediaStatus, Error> { |
048b43af | 103 | self.sg_tape.get_drive_and_media_status() |
a79082a0 DM |
104 | } |
105 | ||
8b2c6f5d | 106 | pub fn forward_space_count_files(&mut self, count: usize) -> Result<(), Error> { |
5d6379f8 | 107 | self.sg_tape.space_filemarks(count.try_into()?) |
8b2c6f5d DM |
108 | } |
109 | ||
110 | pub fn backward_space_count_files(&mut self, count: usize) -> Result<(), Error> { | |
5d6379f8 | 111 | self.sg_tape.space_filemarks(-count.try_into()?) |
8b2c6f5d DM |
112 | } |
113 | ||
7f745967 | 114 | pub fn forward_space_count_records(&mut self, count: usize) -> Result<(), Error> { |
5d6379f8 | 115 | self.sg_tape.space_blocks(count.try_into()?) |
7f745967 DM |
116 | } |
117 | ||
118 | pub fn backward_space_count_records(&mut self, count: usize) -> Result<(), Error> { | |
5d6379f8 DM |
119 | self.sg_tape.space_blocks(-count.try_into()?) |
120 | } | |
121 | ||
122 | /// Position the tape after filemark count. Count 0 means BOT. | |
4de1c42c | 123 | pub fn locate_file(&mut self, position: u64) -> Result<(), Error> { |
bbbf662d | 124 | self.sg_tape.locate_file(position) |
7f745967 DM |
125 | } |
126 | ||
e29f456e DM |
127 | pub fn erase_media(&mut self, fast: bool) -> Result<(), Error> { |
128 | self.sg_tape.erase_media(fast) | |
129 | } | |
130 | ||
4de1c42c | 131 | pub fn load(&mut self) -> Result<(), Error> { |
a79082a0 DM |
132 | self.sg_tape.load() |
133 | } | |
134 | ||
135 | /// Read Cartridge Memory (MAM Attributes) | |
136 | pub fn cartridge_memory(&mut self) -> Result<Vec<MamAttribute>, Error> { | |
137 | self.sg_tape.cartridge_memory() | |
4de1c42c | 138 | } |
a79082a0 DM |
139 | |
140 | /// Read Volume Statistics | |
141 | pub fn volume_statistics(&mut self) -> Result<Lp17VolumeStatistics, Error> { | |
142 | self.sg_tape.volume_statistics() | |
143 | } | |
566b946f DM |
144 | |
145 | /// Lock the drive door | |
4de1c42c TL |
146 | pub fn lock(&mut self) -> Result<(), Error> { |
147 | self.sg_tape | |
148 | .set_medium_removal(false) | |
566b946f DM |
149 | .map_err(|err| format_err!("lock door failed - {}", err)) |
150 | } | |
151 | ||
152 | /// Unlock the drive door | |
4de1c42c TL |
153 | pub fn unlock(&mut self) -> Result<(), Error> { |
154 | self.sg_tape | |
155 | .set_medium_removal(true) | |
566b946f DM |
156 | .map_err(|err| format_err!("unlock door failed - {}", err)) |
157 | } | |
66402cdc DC |
158 | |
159 | /// Returns if a medium is present | |
160 | pub fn medium_present(&mut self) -> bool { | |
161 | self.sg_tape.test_unit_ready().is_ok() | |
162 | } | |
a79082a0 DM |
163 | } |
164 | ||
a79082a0 | 165 | impl TapeDriver for LtoTapeHandle { |
a79082a0 DM |
166 | fn sync(&mut self) -> Result<(), Error> { |
167 | self.sg_tape.sync()?; | |
168 | Ok(()) | |
169 | } | |
170 | ||
171 | /// Go to the end of the recorded media (for appending files). | |
7b11a809 DM |
172 | fn move_to_eom(&mut self, write_missing_eof: bool) -> Result<(), Error> { |
173 | self.sg_tape.move_to_eom(write_missing_eof) | |
a79082a0 DM |
174 | } |
175 | ||
8b2c6f5d | 176 | fn move_to_last_file(&mut self) -> Result<(), Error> { |
7b11a809 DM |
177 | self.move_to_eom(false)?; |
178 | ||
179 | self.sg_tape.check_filemark()?; | |
8b2c6f5d DM |
180 | |
181 | let pos = self.current_file_number()?; | |
182 | ||
183 | if pos == 0 { | |
184 | bail!("move_to_last_file failed - media contains no data"); | |
185 | } | |
186 | ||
187 | if pos == 1 { | |
188 | self.rewind()?; | |
189 | return Ok(()); | |
190 | } | |
191 | ||
192 | self.backward_space_count_files(2)?; | |
193 | self.forward_space_count_files(1)?; | |
194 | ||
195 | Ok(()) | |
a79082a0 DM |
196 | } |
197 | ||
56d36ca4 DC |
198 | fn move_to_file(&mut self, file: u64) -> Result<(), Error> { |
199 | self.locate_file(file) | |
200 | } | |
201 | ||
a79082a0 DM |
202 | fn rewind(&mut self) -> Result<(), Error> { |
203 | self.sg_tape.rewind() | |
204 | } | |
205 | ||
206 | fn current_file_number(&mut self) -> Result<u64, Error> { | |
207 | self.sg_tape.current_file_number() | |
208 | } | |
209 | ||
e29f456e DM |
210 | fn format_media(&mut self, fast: bool) -> Result<(), Error> { |
211 | self.sg_tape.format_media(fast) | |
a79082a0 DM |
212 | } |
213 | ||
318b3106 | 214 | fn read_next_file<'a>(&'a mut self) -> Result<Box<dyn TapeRead + 'a>, BlockReadError> { |
a79082a0 | 215 | let reader = self.sg_tape.open_reader()?; |
318b3106 | 216 | let handle: Box<dyn TapeRead> = Box::new(reader); |
a79082a0 DM |
217 | Ok(handle) |
218 | } | |
219 | ||
220 | fn write_file<'a>(&'a mut self) -> Result<Box<dyn TapeWrite + 'a>, std::io::Error> { | |
221 | let handle = self.sg_tape.open_writer(); | |
222 | Ok(Box::new(handle)) | |
223 | } | |
224 | ||
225 | fn write_media_set_label( | |
226 | &mut self, | |
227 | media_set_label: &MediaSetLabel, | |
228 | key_config: Option<&KeyConfig>, | |
229 | ) -> Result<(), Error> { | |
a79082a0 DM |
230 | let file_number = self.current_file_number()?; |
231 | if file_number != 1 { | |
232 | self.rewind()?; | |
233 | self.forward_space_count_files(1)?; // skip label | |
234 | } | |
235 | ||
236 | let file_number = self.current_file_number()?; | |
237 | if file_number != 1 { | |
4de1c42c TL |
238 | bail!( |
239 | "write_media_set_label failed - got wrong file number ({} != 1)", | |
240 | file_number | |
241 | ); | |
a79082a0 DM |
242 | } |
243 | ||
244 | self.set_encryption(None)?; | |
245 | ||
4de1c42c TL |
246 | { |
247 | // limit handle scope | |
a79082a0 DM |
248 | let mut handle = self.write_file()?; |
249 | ||
250 | let mut value = serde_json::to_value(media_set_label)?; | |
251 | if media_set_label.encryption_key_fingerprint.is_some() { | |
252 | match key_config { | |
253 | Some(key_config) => { | |
254 | value["key-config"] = serde_json::to_value(key_config)?; | |
255 | } | |
256 | None => { | |
257 | bail!("missing encryption key config"); | |
258 | } | |
259 | } | |
260 | } | |
261 | ||
262 | let raw = serde_json::to_string_pretty(&value)?; | |
263 | ||
4de1c42c TL |
264 | let header = |
265 | MediaContentHeader::new(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, raw.len() as u32); | |
a79082a0 DM |
266 | handle.write_header(&header, raw.as_bytes())?; |
267 | handle.finish(false)?; | |
268 | } | |
269 | ||
270 | self.sync()?; // sync data to tape | |
271 | ||
272 | Ok(()) | |
273 | } | |
274 | ||
275 | /// Rewind and put the drive off line (Eject media). | |
276 | fn eject_media(&mut self) -> Result<(), Error> { | |
277 | self.sg_tape.eject() | |
278 | } | |
279 | ||
280 | /// Read Tape Alert Flags | |
281 | fn tape_alert_flags(&mut self) -> Result<TapeAlertFlags, Error> { | |
282 | self.sg_tape.tape_alert_flags() | |
283 | } | |
284 | ||
285 | /// Set or clear encryption key | |
286 | /// | |
287 | /// Note: Only 'root' can read secret encryption keys, so we need | |
288 | /// to spawn setuid binary 'sg-tape-cmd'. | |
289 | fn set_encryption( | |
290 | &mut self, | |
291 | key_fingerprint: Option<(Fingerprint, Uuid)>, | |
292 | ) -> Result<(), Error> { | |
8ee5a5d3 | 293 | if let Some((fingerprint, uuid)) = key_fingerprint { |
ca6e66aa | 294 | let fingerprint = fingerprint.signature(); |
8ee5a5d3 | 295 | let output = run_sg_tape_cmd( |
4de1c42c TL |
296 | "encryption", |
297 | &["--fingerprint", &fingerprint, "--uuid", &uuid.to_string()], | |
298 | self.sg_tape.file_mut().as_raw_fd(), | |
8ee5a5d3 | 299 | )?; |
1dc0df33 | 300 | self.encryption_key_loaded = true; |
8ee5a5d3 DC |
301 | let result: Result<(), String> = serde_json::from_str(&output)?; |
302 | result.map_err(|err| format_err!("{}", err)) | |
a79082a0 | 303 | } else { |
8ee5a5d3 DC |
304 | self.sg_tape.set_encryption(None) |
305 | } | |
a79082a0 DM |
306 | } |
307 | } | |
308 | ||
a79082a0 | 309 | fn run_sg_tape_cmd(subcmd: &str, args: &[&str], fd: RawFd) -> Result<String, Error> { |
4de1c42c TL |
310 | let mut command = |
311 | std::process::Command::new("/usr/lib/x86_64-linux-gnu/proxmox-backup/sg-tape-cmd"); | |
16f6766a FG |
312 | command.args([subcmd]); |
313 | command.args(["--stdin"]); | |
a79082a0 DM |
314 | command.args(args); |
315 | let device_fd = nix::unistd::dup(fd)?; | |
4de1c42c | 316 | command.stdin(unsafe { std::process::Stdio::from_raw_fd(device_fd) }); |
a79082a0 DM |
317 | run_command(command, None) |
318 | } |