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