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