]>
Commit | Line | Data |
---|---|---|
37796ff7 DM |
1 | //! Tape drivers |
2 | ||
fa9c9be7 | 3 | mod virtual_tape; |
83b8949a | 4 | |
a79082a0 DM |
5 | mod lto; |
6 | pub use lto::*; | |
fa9c9be7 | 7 | |
25aa55b5 | 8 | use std::os::unix::io::AsRawFd; |
cd44fb8d | 9 | use std::path::PathBuf; |
25aa55b5 | 10 | |
fa9c9be7 | 11 | use anyhow::{bail, format_err, Error}; |
fe6c1938 | 12 | use ::serde::{Deserialize}; |
feb1645f | 13 | use serde_json::Value; |
fa9c9be7 | 14 | |
2b191385 DM |
15 | use proxmox::{ |
16 | tools::{ | |
17 | Uuid, | |
18 | io::ReadExt, | |
546d2653 DC |
19 | fs::{ |
20 | fchown, | |
21 | file_read_optional_string, | |
22 | replace_file, | |
23 | CreateOptions, | |
24 | } | |
2b191385 DM |
25 | }, |
26 | api::section_config::SectionConfigData, | |
27 | }; | |
fa9c9be7 | 28 | |
c23192d3 WB |
29 | use pbs_datastore::task_log; |
30 | use pbs_datastore::task::TaskState; | |
31 | ||
fa9c9be7 | 32 | use crate::{ |
feb1645f DM |
33 | backup::{ |
34 | Fingerprint, | |
35 | KeyConfig, | |
36 | }, | |
fa9c9be7 DM |
37 | api2::types::{ |
38 | VirtualTapeDrive, | |
a79082a0 | 39 | LtoTapeDrive, |
fa9c9be7 | 40 | }, |
28926247 DC |
41 | server::{ |
42 | send_load_media_email, | |
43 | WorkerTask, | |
44 | }, | |
fa9c9be7 DM |
45 | tape::{ |
46 | TapeWrite, | |
47 | TapeRead, | |
318b3106 | 48 | BlockReadError, |
fe6c1938 | 49 | MediaId, |
109ccd30 | 50 | drive::lto::TapeAlertFlags, |
fa9c9be7 | 51 | file_formats::{ |
a78348ac | 52 | PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0, |
fa9c9be7 | 53 | PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, |
a78348ac | 54 | MediaLabel, |
fa9c9be7 DM |
55 | MediaSetLabel, |
56 | MediaContentHeader, | |
57 | }, | |
58 | changer::{ | |
59 | MediaChange, | |
37796ff7 | 60 | MtxMediaChanger, |
fa9c9be7 DM |
61 | }, |
62 | }, | |
63 | }; | |
64 | ||
fa9c9be7 DM |
65 | /// Tape driver interface |
66 | pub trait TapeDriver { | |
67 | ||
68 | /// Flush all data to the tape | |
69 | fn sync(&mut self) -> Result<(), Error>; | |
70 | ||
71 | /// Rewind the tape | |
72 | fn rewind(&mut self) -> Result<(), Error>; | |
73 | ||
74 | /// Move to end of recorded data | |
75 | /// | |
7b11a809 DM |
76 | /// We assume this flushes the tape write buffer. if |
77 | /// write_missing_eof is true, we verify that there is a filemark | |
78 | /// at the end. If not, we write one. | |
79 | fn move_to_eom(&mut self, write_missing_eof: bool) -> Result<(), Error>; | |
fa9c9be7 | 80 | |
20cc25d7 | 81 | /// Move to last file |
8b2c6f5d | 82 | fn move_to_last_file(&mut self) -> Result<(), Error>; |
20cc25d7 | 83 | |
56d36ca4 DC |
84 | /// Move to given file nr |
85 | fn move_to_file(&mut self, file: u64) -> Result<(), Error>; | |
86 | ||
fa9c9be7 | 87 | /// Current file number |
26aa9aca | 88 | fn current_file_number(&mut self) -> Result<u64, Error>; |
fa9c9be7 DM |
89 | |
90 | /// Completely erase the media | |
e29f456e | 91 | fn format_media(&mut self, fast: bool) -> Result<(), Error>; |
fa9c9be7 DM |
92 | |
93 | /// Read/Open the next file | |
318b3106 | 94 | fn read_next_file<'a>(&'a mut self) -> Result<Box<dyn TapeRead + 'a>, BlockReadError>; |
fa9c9be7 DM |
95 | |
96 | /// Write/Append a new file | |
97 | fn write_file<'a>(&'a mut self) -> Result<Box<dyn TapeWrite + 'a>, std::io::Error>; | |
98 | ||
99 | /// Write label to tape (erase tape content) | |
fe6c1938 | 100 | fn label_tape(&mut self, label: &MediaLabel) -> Result<(), Error> { |
fa9c9be7 | 101 | |
619554af DM |
102 | self.set_encryption(None)?; |
103 | ||
e29f456e | 104 | self.format_media(true)?; // this rewinds the tape |
fa9c9be7 DM |
105 | |
106 | let raw = serde_json::to_string_pretty(&serde_json::to_value(&label)?)?; | |
107 | ||
a78348ac | 108 | let header = MediaContentHeader::new(PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0, raw.len() as u32); |
fa9c9be7 DM |
109 | |
110 | { | |
111 | let mut writer = self.write_file()?; | |
112 | writer.write_header(&header, raw.as_bytes())?; | |
113 | writer.finish(false)?; | |
114 | } | |
115 | ||
116 | self.sync()?; // sync data to tape | |
117 | ||
fe6c1938 | 118 | Ok(()) |
fa9c9be7 DM |
119 | } |
120 | ||
121 | /// Write the media set label to tape | |
feb1645f DM |
122 | /// |
123 | /// If the media-set is encrypted, we also store the encryption | |
124 | /// key_config, so that it is possible to restore the key. | |
125 | fn write_media_set_label( | |
126 | &mut self, | |
127 | media_set_label: &MediaSetLabel, | |
128 | key_config: Option<&KeyConfig>, | |
129 | ) -> Result<(), Error>; | |
fa9c9be7 DM |
130 | |
131 | /// Read the media label | |
132 | /// | |
feb1645f DM |
133 | /// This tries to read both media labels (label and |
134 | /// media_set_label). Also returns the optional encryption key configuration. | |
135 | fn read_label(&mut self) -> Result<(Option<MediaId>, Option<KeyConfig>), Error> { | |
fa9c9be7 DM |
136 | |
137 | self.rewind()?; | |
138 | ||
fe6c1938 | 139 | let label = { |
318b3106 DM |
140 | let mut reader = match self.read_next_file() { |
141 | Err(BlockReadError::EndOfStream) => { | |
142 | return Ok((None, None)); // tape is empty | |
143 | } | |
144 | Err(BlockReadError::EndOfFile) => { | |
145 | bail!("got unexpected filemark at BOT"); | |
146 | } | |
147 | Err(BlockReadError::Error(err)) => { | |
148 | return Err(err.into()); | |
149 | } | |
150 | Ok(reader) => reader, | |
fa9c9be7 DM |
151 | }; |
152 | ||
153 | let header: MediaContentHeader = unsafe { reader.read_le_value()? }; | |
a78348ac | 154 | header.check(PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0, 1, 64*1024)?; |
fa9c9be7 DM |
155 | let data = reader.read_exact_allocated(header.size as usize)?; |
156 | ||
a78348ac | 157 | let label: MediaLabel = serde_json::from_slice(&data) |
fa9c9be7 DM |
158 | .map_err(|err| format_err!("unable to parse drive label - {}", err))?; |
159 | ||
160 | // make sure we read the EOF marker | |
161 | if reader.skip_to_end()? != 0 { | |
162 | bail!("got unexpected data after label"); | |
163 | } | |
164 | ||
fe6c1938 | 165 | label |
fa9c9be7 DM |
166 | }; |
167 | ||
fe6c1938 | 168 | let mut media_id = MediaId { label, media_set_label: None }; |
fa9c9be7 | 169 | |
fe6c1938 | 170 | // try to read MediaSet label |
318b3106 DM |
171 | let mut reader = match self.read_next_file() { |
172 | Err(BlockReadError::EndOfStream) => { | |
173 | return Ok((Some(media_id), None)); | |
174 | } | |
175 | Err(BlockReadError::EndOfFile) => { | |
176 | bail!("got unexpected filemark after label"); | |
177 | } | |
178 | Err(BlockReadError::Error(err)) => { | |
179 | return Err(err.into()); | |
180 | } | |
181 | Ok(reader) => reader, | |
fa9c9be7 DM |
182 | }; |
183 | ||
184 | let header: MediaContentHeader = unsafe { reader.read_le_value()? }; | |
185 | header.check(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, 1, 64*1024)?; | |
186 | let data = reader.read_exact_allocated(header.size as usize)?; | |
187 | ||
feb1645f DM |
188 | let mut data: Value = serde_json::from_slice(&data) |
189 | .map_err(|err| format_err!("unable to parse media set label - {}", err))?; | |
190 | ||
191 | let key_config_value = data["key-config"].take(); | |
192 | let key_config: Option<KeyConfig> = if !key_config_value.is_null() { | |
193 | Some(serde_json::from_value(key_config_value)?) | |
194 | } else { | |
195 | None | |
196 | }; | |
197 | ||
198 | let media_set_label: MediaSetLabel = serde_json::from_value(data) | |
fa9c9be7 DM |
199 | .map_err(|err| format_err!("unable to parse media set label - {}", err))?; |
200 | ||
201 | // make sure we read the EOF marker | |
202 | if reader.skip_to_end()? != 0 { | |
203 | bail!("got unexpected data after media set label"); | |
204 | } | |
205 | ||
fe6c1938 | 206 | media_id.media_set_label = Some(media_set_label); |
fa9c9be7 | 207 | |
feb1645f | 208 | Ok((Some(media_id), key_config)) |
fa9c9be7 DM |
209 | } |
210 | ||
211 | /// Eject media | |
212 | fn eject_media(&mut self) -> Result<(), Error>; | |
5843268c DM |
213 | |
214 | /// Read Tape Alert Flags | |
215 | /// | |
216 | /// This make only sense for real LTO drives. Virtual tape drives should | |
217 | /// simply return empty flags (default). | |
218 | fn tape_alert_flags(&mut self) -> Result<TapeAlertFlags, Error> { | |
219 | Ok(TapeAlertFlags::empty()) | |
220 | } | |
d5a48b5c DM |
221 | |
222 | /// Set or clear encryption key | |
2b191385 DM |
223 | /// |
224 | /// We use the media_set_uuid to XOR the secret key with the | |
d1d74c43 | 225 | /// uuid (first 16 bytes), so that each media set uses an unique |
2b191385 DM |
226 | /// key for encryption. |
227 | fn set_encryption( | |
228 | &mut self, | |
229 | key_fingerprint: Option<(Fingerprint, Uuid)>, | |
230 | ) -> Result<(), Error> { | |
d5a48b5c DM |
231 | if key_fingerprint.is_some() { |
232 | bail!("drive does not support encryption"); | |
233 | } | |
234 | Ok(()) | |
235 | } | |
fa9c9be7 DM |
236 | } |
237 | ||
284eb5da | 238 | /// Get the media changer (MediaChange + name) associated with a tape drive. |
fa9c9be7 | 239 | /// |
284eb5da | 240 | /// Returns Ok(None) if the drive has no associated changer device. |
75656a78 DM |
241 | /// |
242 | /// Note: This may return the drive name as changer-name if the drive | |
243 | /// implements some kind of internal changer (which is true for our | |
244 | /// 'virtual' drive implementation). | |
fa9c9be7 DM |
245 | pub fn media_changer( |
246 | config: &SectionConfigData, | |
247 | drive: &str, | |
284eb5da | 248 | ) -> Result<Option<(Box<dyn MediaChange>, String)>, Error> { |
fa9c9be7 DM |
249 | |
250 | match config.sections.get(drive) { | |
251 | Some((section_type_name, config)) => { | |
252 | match section_type_name.as_ref() { | |
253 | "virtual" => { | |
254 | let tape = VirtualTapeDrive::deserialize(config)?; | |
284eb5da | 255 | Ok(Some((Box::new(tape), drive.to_string()))) |
fa9c9be7 | 256 | } |
a79082a0 DM |
257 | "lto" => { |
258 | let drive_config = LtoTapeDrive::deserialize(config)?; | |
6fe16039 | 259 | match drive_config.changer { |
fa9c9be7 | 260 | Some(ref changer_name) => { |
6fe16039 | 261 | let changer = MtxMediaChanger::with_drive_config(&drive_config)?; |
fa9c9be7 | 262 | let changer_name = changer_name.to_string(); |
6fe16039 | 263 | Ok(Some((Box::new(changer), changer_name))) |
fa9c9be7 | 264 | } |
284eb5da | 265 | None => Ok(None), |
fa9c9be7 DM |
266 | } |
267 | } | |
284eb5da | 268 | _ => bail!("unknown drive type '{}' - internal error"), |
fa9c9be7 DM |
269 | } |
270 | } | |
271 | None => { | |
272 | bail!("no such drive '{}'", drive); | |
273 | } | |
274 | } | |
275 | } | |
276 | ||
284eb5da DM |
277 | /// Get the media changer (MediaChange + name) associated with a tape drive. |
278 | /// | |
279 | /// This fail if the drive has no associated changer device. | |
280 | pub fn required_media_changer( | |
281 | config: &SectionConfigData, | |
282 | drive: &str, | |
283 | ) -> Result<(Box<dyn MediaChange>, String), Error> { | |
284 | match media_changer(config, drive) { | |
285 | Ok(Some(result)) => { | |
38556bf6 | 286 | Ok(result) |
284eb5da DM |
287 | } |
288 | Ok(None) => { | |
289 | bail!("drive '{}' has no associated changer device", drive); | |
290 | }, | |
291 | Err(err) => { | |
38556bf6 | 292 | Err(err) |
284eb5da DM |
293 | } |
294 | } | |
295 | } | |
296 | ||
edda5039 | 297 | /// Opens a tape drive (this fails if there is no media loaded) |
fa9c9be7 DM |
298 | pub fn open_drive( |
299 | config: &SectionConfigData, | |
300 | drive: &str, | |
301 | ) -> Result<Box<dyn TapeDriver>, Error> { | |
302 | ||
303 | match config.sections.get(drive) { | |
304 | Some((section_type_name, config)) => { | |
305 | match section_type_name.as_ref() { | |
306 | "virtual" => { | |
307 | let tape = VirtualTapeDrive::deserialize(config)?; | |
d5a48b5c DM |
308 | let handle = tape.open()?; |
309 | Ok(Box::new(handle)) | |
fa9c9be7 | 310 | } |
a79082a0 DM |
311 | "lto" => { |
312 | let tape = LtoTapeDrive::deserialize(config)?; | |
d5a48b5c | 313 | let handle = tape.open()?; |
fa9c9be7 DM |
314 | Ok(Box::new(handle)) |
315 | } | |
284eb5da | 316 | _ => bail!("unknown drive type '{}' - internal error"), |
fa9c9be7 DM |
317 | } |
318 | } | |
319 | None => { | |
320 | bail!("no such drive '{}'", drive); | |
321 | } | |
322 | } | |
323 | } | |
324 | ||
9ac8b73e DC |
325 | #[derive(PartialEq, Eq)] |
326 | enum TapeRequestError { | |
327 | None, | |
328 | EmptyTape, | |
329 | OpenFailed(String), | |
330 | WrongLabel(String), | |
331 | ReadFailed(String), | |
332 | } | |
333 | ||
334 | impl std::fmt::Display for TapeRequestError { | |
335 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
336 | match self { | |
337 | TapeRequestError::None => { | |
338 | write!(f, "no error") | |
339 | }, | |
340 | TapeRequestError::OpenFailed(reason) => { | |
341 | write!(f, "tape open failed - {}", reason) | |
342 | } | |
343 | TapeRequestError::WrongLabel(label) => { | |
344 | write!(f, "wrong media label {}", label) | |
345 | } | |
346 | TapeRequestError::EmptyTape => { | |
347 | write!(f, "found empty media without label (please label all tapes first)") | |
348 | } | |
349 | TapeRequestError::ReadFailed(reason) => { | |
350 | write!(f, "tape read failed - {}", reason) | |
351 | } | |
352 | } | |
353 | } | |
354 | } | |
355 | ||
fa9c9be7 DM |
356 | /// Requests a specific 'media' to be inserted into 'drive'. Within a |
357 | /// loop, this then tries to read the media label and waits until it | |
358 | /// finds the requested media. | |
359 | /// | |
360 | /// Returns a handle to the opened drive and the media labels. | |
361 | pub fn request_and_load_media( | |
ff58c519 | 362 | worker: &WorkerTask, |
fa9c9be7 DM |
363 | config: &SectionConfigData, |
364 | drive: &str, | |
a78348ac | 365 | label: &MediaLabel, |
c9793d47 | 366 | notify_email: &Option<String>, |
fa9c9be7 DM |
367 | ) -> Result<( |
368 | Box<dyn TapeDriver>, | |
fe6c1938 | 369 | MediaId, |
fa9c9be7 DM |
370 | ), Error> { |
371 | ||
ff58c519 | 372 | let check_label = |handle: &mut dyn TapeDriver, uuid: &proxmox::tools::Uuid| { |
feb1645f | 373 | if let Ok((Some(media_id), _)) = handle.read_label() { |
271764de DM |
374 | task_log!( |
375 | worker, | |
284eb5da | 376 | "found media label {} ({})", |
8446fbca | 377 | media_id.label.label_text, |
271764de DM |
378 | media_id.label.uuid, |
379 | ); | |
380 | ||
ff58c519 DM |
381 | if media_id.label.uuid == *uuid { |
382 | return Ok(media_id); | |
383 | } | |
384 | } | |
385 | bail!("read label failed (please label all tapes first)"); | |
386 | }; | |
387 | ||
fa9c9be7 DM |
388 | match config.sections.get(drive) { |
389 | Some((section_type_name, config)) => { | |
390 | match section_type_name.as_ref() { | |
391 | "virtual" => { | |
ff58c519 | 392 | let mut tape = VirtualTapeDrive::deserialize(config)?; |
fa9c9be7 | 393 | |
8446fbca | 394 | let label_text = label.label_text.clone(); |
fa9c9be7 | 395 | |
8446fbca | 396 | tape.load_media(&label_text)?; |
fa9c9be7 | 397 | |
ff58c519 | 398 | let mut handle: Box<dyn TapeDriver> = Box::new(tape.open()?); |
fa9c9be7 | 399 | |
ff58c519 DM |
400 | let media_id = check_label(handle.as_mut(), &label.uuid)?; |
401 | ||
38556bf6 | 402 | Ok((handle, media_id)) |
fa9c9be7 | 403 | } |
a79082a0 DM |
404 | "lto" => { |
405 | let drive_config = LtoTapeDrive::deserialize(config)?; | |
ff58c519 | 406 | |
8446fbca | 407 | let label_text = label.label_text.clone(); |
fa9c9be7 | 408 | |
6fe16039 | 409 | if drive_config.changer.is_some() { |
fa9c9be7 | 410 | |
3fbf2311 DM |
411 | task_log!(worker, "loading media '{}' into drive '{}'", label_text, drive); |
412 | ||
6fe16039 | 413 | let mut changer = MtxMediaChanger::with_drive_config(&drive_config)?; |
8446fbca | 414 | changer.load_media(&label_text)?; |
ff58c519 | 415 | |
6fe16039 | 416 | let mut handle: Box<dyn TapeDriver> = Box::new(drive_config.open()?); |
ff58c519 DM |
417 | |
418 | let media_id = check_label(handle.as_mut(), &label.uuid)?; | |
419 | ||
420 | return Ok((handle, media_id)); | |
421 | } | |
422 | ||
9ac8b73e DC |
423 | let mut last_error = TapeRequestError::None; |
424 | ||
425 | let update_and_log_request_error = | |
426 | |old: &mut TapeRequestError, new: TapeRequestError| -> Result<(), Error> | |
427 | { | |
428 | if new != *old { | |
429 | task_log!(worker, "{}", new); | |
430 | task_log!( | |
431 | worker, | |
432 | "Please insert media '{}' into drive '{}'", | |
433 | label_text, | |
434 | drive | |
435 | ); | |
436 | if let Some(to) = notify_email { | |
437 | send_load_media_email( | |
438 | drive, | |
439 | &label_text, | |
440 | to, | |
441 | Some(new.to_string()), | |
442 | )?; | |
443 | } | |
444 | *old = new; | |
445 | } | |
446 | Ok(()) | |
447 | }; | |
78593b5b | 448 | |
fa9c9be7 | 449 | loop { |
271764de DM |
450 | worker.check_abort()?; |
451 | ||
9ac8b73e | 452 | if last_error != TapeRequestError::None { |
78593b5b DC |
453 | for _ in 0..50 { // delay 5 seconds |
454 | worker.check_abort()?; | |
455 | std::thread::sleep(std::time::Duration::from_millis(100)); | |
456 | } | |
9ac8b73e DC |
457 | } else { |
458 | task_log!( | |
459 | worker, | |
460 | "Checking for media '{}' in drive '{}'", | |
461 | label_text, | |
462 | drive | |
463 | ); | |
78593b5b DC |
464 | } |
465 | ||
6fe16039 | 466 | let mut handle = match drive_config.open() { |
fa9c9be7 | 467 | Ok(handle) => handle, |
81764111 | 468 | Err(err) => { |
9ac8b73e DC |
469 | update_and_log_request_error( |
470 | &mut last_error, | |
471 | TapeRequestError::OpenFailed(err.to_string()), | |
472 | )?; | |
fa9c9be7 DM |
473 | continue; |
474 | } | |
475 | }; | |
476 | ||
9ac8b73e DC |
477 | let request_error = match handle.read_label() { |
478 | Ok((Some(media_id), _)) if media_id.label.uuid == label.uuid => { | |
479 | task_log!( | |
480 | worker, | |
481 | "found media label {} ({})", | |
482 | media_id.label.label_text, | |
483 | media_id.label.uuid.to_string(), | |
484 | ); | |
485 | return Ok((Box::new(handle), media_id)); | |
486 | } | |
feb1645f | 487 | Ok((Some(media_id), _)) => { |
9ac8b73e DC |
488 | let label_string = format!( |
489 | "{} ({})", | |
490 | media_id.label.label_text, | |
491 | media_id.label.uuid.to_string(), | |
492 | ); | |
493 | TapeRequestError::WrongLabel(label_string) | |
ff58c519 | 494 | } |
feb1645f | 495 | Ok((None, _)) => { |
9ac8b73e | 496 | TapeRequestError::EmptyTape |
fa9c9be7 | 497 | } |
81764111 | 498 | Err(err) => { |
9ac8b73e | 499 | TapeRequestError::ReadFailed(err.to_string()) |
81764111 | 500 | } |
9ac8b73e DC |
501 | }; |
502 | ||
503 | update_and_log_request_error(&mut last_error, request_error)?; | |
fa9c9be7 DM |
504 | } |
505 | } | |
506 | _ => bail!("drive type '{}' not implemented!"), | |
507 | } | |
508 | } | |
509 | None => { | |
510 | bail!("no such drive '{}'", drive); | |
511 | } | |
512 | } | |
513 | } | |
25aa55b5 | 514 | |
e5950360 DC |
515 | #[derive(thiserror::Error, Debug)] |
516 | pub enum TapeLockError { | |
517 | #[error("timeout while trying to lock")] | |
518 | TimeOut, | |
519 | #[error("{0}")] | |
520 | Other(#[from] Error), | |
521 | } | |
522 | ||
523 | impl From<std::io::Error> for TapeLockError { | |
524 | fn from(error: std::io::Error) -> Self { | |
525 | Self::Other(error.into()) | |
526 | } | |
527 | } | |
528 | ||
d1d74c43 | 529 | /// Acquires an exclusive lock for the tape device |
25aa55b5 DM |
530 | /// |
531 | /// Basically calls lock_device_path() using the configured drive path. | |
532 | pub fn lock_tape_device( | |
533 | config: &SectionConfigData, | |
534 | drive: &str, | |
e5950360 | 535 | ) -> Result<DeviceLockGuard, TapeLockError> { |
546d2653 | 536 | let path = tape_device_path(config, drive)?; |
e5950360 DC |
537 | lock_device_path(&path).map_err(|err| match err { |
538 | TapeLockError::Other(err) => { | |
539 | TapeLockError::Other(format_err!("unable to lock drive '{}' - {}", drive, err)) | |
540 | } | |
541 | other => other, | |
542 | }) | |
546d2653 DC |
543 | } |
544 | ||
545 | /// Writes the given state for the specified drive | |
546 | /// | |
547 | /// This function does not lock, so make sure the drive is locked | |
548 | pub fn set_tape_device_state( | |
549 | drive: &str, | |
550 | state: &str, | |
551 | ) -> Result<(), Error> { | |
cd44fb8d DM |
552 | |
553 | let mut path = PathBuf::from(crate::tape::DRIVE_STATE_DIR); | |
554 | path.push(drive); | |
546d2653 DC |
555 | |
556 | let backup_user = crate::backup::backup_user()?; | |
557 | let mode = nix::sys::stat::Mode::from_bits_truncate(0o0644); | |
558 | let options = CreateOptions::new() | |
559 | .perm(mode) | |
560 | .owner(backup_user.uid) | |
561 | .group(backup_user.gid); | |
562 | ||
563 | replace_file(path, state.as_bytes(), options) | |
564 | } | |
25aa55b5 | 565 | |
546d2653 DC |
566 | /// Get the device state |
567 | pub fn get_tape_device_state( | |
568 | config: &SectionConfigData, | |
569 | drive: &str, | |
570 | ) -> Result<Option<String>, Error> { | |
a0cd0f9c | 571 | let path = format!("{}/{}", crate::tape::DRIVE_STATE_DIR, drive); |
546d2653 DC |
572 | let state = file_read_optional_string(path)?; |
573 | ||
574 | let device_path = tape_device_path(config, drive)?; | |
575 | if test_device_path_lock(&device_path)? { | |
576 | Ok(state) | |
577 | } else { | |
578 | Ok(None) | |
579 | } | |
580 | } | |
581 | ||
582 | fn tape_device_path( | |
583 | config: &SectionConfigData, | |
584 | drive: &str, | |
585 | ) -> Result<String, Error> { | |
25aa55b5 DM |
586 | match config.sections.get(drive) { |
587 | Some((section_type_name, config)) => { | |
588 | let path = match section_type_name.as_ref() { | |
589 | "virtual" => { | |
590 | VirtualTapeDrive::deserialize(config)?.path | |
591 | } | |
a79082a0 DM |
592 | "lto" => { |
593 | LtoTapeDrive::deserialize(config)?.path | |
25aa55b5 DM |
594 | } |
595 | _ => bail!("unknown drive type '{}' - internal error"), | |
596 | }; | |
546d2653 | 597 | Ok(path) |
25aa55b5 DM |
598 | } |
599 | None => { | |
600 | bail!("no such drive '{}'", drive); | |
601 | } | |
602 | } | |
603 | } | |
604 | ||
605 | pub struct DeviceLockGuard(std::fs::File); | |
606 | ||
d1d74c43 | 607 | // Acquires an exclusive lock on `device_path` |
25aa55b5 DM |
608 | // |
609 | // Uses systemd escape_unit to compute a file name from `device_path`, the try | |
610 | // to lock `/var/lock/<name>`. | |
e5950360 | 611 | fn lock_device_path(device_path: &str) -> Result<DeviceLockGuard, TapeLockError> { |
25aa55b5 DM |
612 | |
613 | let lock_name = crate::tools::systemd::escape_unit(device_path, true); | |
614 | ||
a0cd0f9c | 615 | let mut path = std::path::PathBuf::from(crate::tape::DRIVE_LOCK_DIR); |
25aa55b5 DM |
616 | path.push(lock_name); |
617 | ||
618 | let timeout = std::time::Duration::new(10, 0); | |
619 | let mut file = std::fs::OpenOptions::new().create(true).append(true).open(path)?; | |
e5950360 DC |
620 | if let Err(err) = proxmox::tools::fs::lock_file(&mut file, true, Some(timeout)) { |
621 | if err.kind() == std::io::ErrorKind::Interrupted { | |
622 | return Err(TapeLockError::TimeOut); | |
623 | } else { | |
624 | return Err(err.into()); | |
625 | } | |
626 | } | |
25aa55b5 DM |
627 | |
628 | let backup_user = crate::backup::backup_user()?; | |
629 | fchown(file.as_raw_fd(), Some(backup_user.uid), Some(backup_user.gid))?; | |
630 | ||
631 | Ok(DeviceLockGuard(file)) | |
632 | } | |
33c06b33 DC |
633 | |
634 | // Same logic as lock_device_path, but uses a timeout of 0, making it | |
635 | // non-blocking, and returning if the file is locked or not | |
636 | fn test_device_path_lock(device_path: &str) -> Result<bool, Error> { | |
637 | ||
638 | let lock_name = crate::tools::systemd::escape_unit(device_path, true); | |
639 | ||
a0cd0f9c | 640 | let mut path = std::path::PathBuf::from(crate::tape::DRIVE_LOCK_DIR); |
33c06b33 DC |
641 | path.push(lock_name); |
642 | ||
643 | let timeout = std::time::Duration::new(0, 0); | |
644 | let mut file = std::fs::OpenOptions::new().create(true).append(true).open(path)?; | |
645 | match proxmox::tools::fs::lock_file(&mut file, true, Some(timeout)) { | |
646 | // file was not locked, continue | |
647 | Ok(()) => {}, | |
648 | // file was locked, return true | |
649 | Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => return Ok(true), | |
650 | Err(err) => bail!("{}", err), | |
651 | } | |
652 | ||
653 | let backup_user = crate::backup::backup_user()?; | |
654 | fchown(file.as_raw_fd(), Some(backup_user.uid), Some(backup_user.gid))?; | |
655 | ||
656 | Ok(false) | |
657 | } |