]> git.proxmox.com Git - proxmox-backup.git/blob - src/tape/drive/mod.rs
change tape drive lock path
[proxmox-backup.git] / src / tape / drive / mod.rs
1 //! Tape drivers
2
3 mod virtual_tape;
4
5 mod lto;
6 pub use lto::*;
7
8 use std::os::unix::io::AsRawFd;
9 use std::path::PathBuf;
10
11 use anyhow::{bail, format_err, Error};
12 use ::serde::{Deserialize};
13 use serde_json::Value;
14
15 use proxmox::{
16 tools::{
17 Uuid,
18 io::ReadExt,
19 fs::{
20 fchown,
21 file_read_optional_string,
22 replace_file,
23 CreateOptions,
24 }
25 },
26 api::section_config::SectionConfigData,
27 };
28
29 use pbs_datastore::task_log;
30 use pbs_datastore::task::TaskState;
31
32 use crate::{
33 backup::{
34 Fingerprint,
35 KeyConfig,
36 },
37 api2::types::{
38 VirtualTapeDrive,
39 LtoTapeDrive,
40 },
41 server::{
42 send_load_media_email,
43 WorkerTask,
44 },
45 tape::{
46 TapeWrite,
47 TapeRead,
48 BlockReadError,
49 MediaId,
50 drive::lto::TapeAlertFlags,
51 file_formats::{
52 PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0,
53 PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0,
54 MediaLabel,
55 MediaSetLabel,
56 MediaContentHeader,
57 },
58 changer::{
59 MediaChange,
60 MtxMediaChanger,
61 },
62 },
63 };
64
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 ///
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>;
80
81 /// Move to last file
82 fn move_to_last_file(&mut self) -> Result<(), Error>;
83
84 /// Move to given file nr
85 fn move_to_file(&mut self, file: u64) -> Result<(), Error>;
86
87 /// Current file number
88 fn current_file_number(&mut self) -> Result<u64, Error>;
89
90 /// Completely erase the media
91 fn format_media(&mut self, fast: bool) -> Result<(), Error>;
92
93 /// Read/Open the next file
94 fn read_next_file<'a>(&'a mut self) -> Result<Box<dyn TapeRead + 'a>, BlockReadError>;
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)
100 fn label_tape(&mut self, label: &MediaLabel) -> Result<(), Error> {
101
102 self.set_encryption(None)?;
103
104 self.format_media(true)?; // this rewinds the tape
105
106 let raw = serde_json::to_string_pretty(&serde_json::to_value(&label)?)?;
107
108 let header = MediaContentHeader::new(PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0, raw.len() as u32);
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
118 Ok(())
119 }
120
121 /// Write the media set label to tape
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>;
130
131 /// Read the media label
132 ///
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> {
136
137 self.rewind()?;
138
139 let label = {
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,
151 };
152
153 let header: MediaContentHeader = unsafe { reader.read_le_value()? };
154 header.check(PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0, 1, 64*1024)?;
155 let data = reader.read_exact_allocated(header.size as usize)?;
156
157 let label: MediaLabel = serde_json::from_slice(&data)
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
165 label
166 };
167
168 let mut media_id = MediaId { label, media_set_label: None };
169
170 // try to read MediaSet label
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,
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
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)
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
206 media_id.media_set_label = Some(media_set_label);
207
208 Ok((Some(media_id), key_config))
209 }
210
211 /// Eject media
212 fn eject_media(&mut self) -> Result<(), Error>;
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 }
221
222 /// Set or clear encryption key
223 ///
224 /// We use the media_set_uuid to XOR the secret key with the
225 /// uuid (first 16 bytes), so that each media set uses an unique
226 /// key for encryption.
227 fn set_encryption(
228 &mut self,
229 key_fingerprint: Option<(Fingerprint, Uuid)>,
230 ) -> Result<(), Error> {
231 if key_fingerprint.is_some() {
232 bail!("drive does not support encryption");
233 }
234 Ok(())
235 }
236 }
237
238 /// Get the media changer (MediaChange + name) associated with a tape drive.
239 ///
240 /// Returns Ok(None) if the drive has no associated changer device.
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).
245 pub fn media_changer(
246 config: &SectionConfigData,
247 drive: &str,
248 ) -> Result<Option<(Box<dyn MediaChange>, String)>, Error> {
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)?;
255 Ok(Some((Box::new(tape), drive.to_string())))
256 }
257 "lto" => {
258 let drive_config = LtoTapeDrive::deserialize(config)?;
259 match drive_config.changer {
260 Some(ref changer_name) => {
261 let changer = MtxMediaChanger::with_drive_config(&drive_config)?;
262 let changer_name = changer_name.to_string();
263 Ok(Some((Box::new(changer), changer_name)))
264 }
265 None => Ok(None),
266 }
267 }
268 _ => bail!("unknown drive type '{}' - internal error"),
269 }
270 }
271 None => {
272 bail!("no such drive '{}'", drive);
273 }
274 }
275 }
276
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)) => {
286 Ok(result)
287 }
288 Ok(None) => {
289 bail!("drive '{}' has no associated changer device", drive);
290 },
291 Err(err) => {
292 Err(err)
293 }
294 }
295 }
296
297 /// Opens a tape drive (this fails if there is no media loaded)
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)?;
308 let handle = tape.open()?;
309 Ok(Box::new(handle))
310 }
311 "lto" => {
312 let tape = LtoTapeDrive::deserialize(config)?;
313 let handle = tape.open()?;
314 Ok(Box::new(handle))
315 }
316 _ => bail!("unknown drive type '{}' - internal error"),
317 }
318 }
319 None => {
320 bail!("no such drive '{}'", drive);
321 }
322 }
323 }
324
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
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(
362 worker: &WorkerTask,
363 config: &SectionConfigData,
364 drive: &str,
365 label: &MediaLabel,
366 notify_email: &Option<String>,
367 ) -> Result<(
368 Box<dyn TapeDriver>,
369 MediaId,
370 ), Error> {
371
372 let check_label = |handle: &mut dyn TapeDriver, uuid: &proxmox::tools::Uuid| {
373 if let Ok((Some(media_id), _)) = handle.read_label() {
374 task_log!(
375 worker,
376 "found media label {} ({})",
377 media_id.label.label_text,
378 media_id.label.uuid,
379 );
380
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
388 match config.sections.get(drive) {
389 Some((section_type_name, config)) => {
390 match section_type_name.as_ref() {
391 "virtual" => {
392 let mut tape = VirtualTapeDrive::deserialize(config)?;
393
394 let label_text = label.label_text.clone();
395
396 tape.load_media(&label_text)?;
397
398 let mut handle: Box<dyn TapeDriver> = Box::new(tape.open()?);
399
400 let media_id = check_label(handle.as_mut(), &label.uuid)?;
401
402 Ok((handle, media_id))
403 }
404 "lto" => {
405 let drive_config = LtoTapeDrive::deserialize(config)?;
406
407 let label_text = label.label_text.clone();
408
409 if drive_config.changer.is_some() {
410
411 task_log!(worker, "loading media '{}' into drive '{}'", label_text, drive);
412
413 let mut changer = MtxMediaChanger::with_drive_config(&drive_config)?;
414 changer.load_media(&label_text)?;
415
416 let mut handle: Box<dyn TapeDriver> = Box::new(drive_config.open()?);
417
418 let media_id = check_label(handle.as_mut(), &label.uuid)?;
419
420 return Ok((handle, media_id));
421 }
422
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 };
448
449 loop {
450 worker.check_abort()?;
451
452 if last_error != TapeRequestError::None {
453 for _ in 0..50 { // delay 5 seconds
454 worker.check_abort()?;
455 std::thread::sleep(std::time::Duration::from_millis(100));
456 }
457 } else {
458 task_log!(
459 worker,
460 "Checking for media '{}' in drive '{}'",
461 label_text,
462 drive
463 );
464 }
465
466 let mut handle = match drive_config.open() {
467 Ok(handle) => handle,
468 Err(err) => {
469 update_and_log_request_error(
470 &mut last_error,
471 TapeRequestError::OpenFailed(err.to_string()),
472 )?;
473 continue;
474 }
475 };
476
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 }
487 Ok((Some(media_id), _)) => {
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)
494 }
495 Ok((None, _)) => {
496 TapeRequestError::EmptyTape
497 }
498 Err(err) => {
499 TapeRequestError::ReadFailed(err.to_string())
500 }
501 };
502
503 update_and_log_request_error(&mut last_error, request_error)?;
504 }
505 }
506 _ => bail!("drive type '{}' not implemented!"),
507 }
508 }
509 None => {
510 bail!("no such drive '{}'", drive);
511 }
512 }
513 }
514
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
529 /// Acquires an exclusive lock for the tape device
530 ///
531 /// Basically calls lock_device_path() using the configured drive path.
532 pub fn lock_tape_device(
533 config: &SectionConfigData,
534 drive: &str,
535 ) -> Result<DeviceLockGuard, TapeLockError> {
536 let path = tape_device_path(config, drive)?;
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 })
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> {
552
553 let mut path = PathBuf::from(crate::tape::DRIVE_STATE_DIR);
554 path.push(drive);
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 }
565
566 /// Get the device state
567 pub fn get_tape_device_state(
568 config: &SectionConfigData,
569 drive: &str,
570 ) -> Result<Option<String>, Error> {
571 let path = format!("{}/{}", crate::tape::DRIVE_STATE_DIR, drive);
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> {
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 }
592 "lto" => {
593 LtoTapeDrive::deserialize(config)?.path
594 }
595 _ => bail!("unknown drive type '{}' - internal error"),
596 };
597 Ok(path)
598 }
599 None => {
600 bail!("no such drive '{}'", drive);
601 }
602 }
603 }
604
605 pub struct DeviceLockGuard(std::fs::File);
606
607 // Acquires an exclusive lock on `device_path`
608 //
609 // Uses systemd escape_unit to compute a file name from `device_path`, the try
610 // to lock `/var/lock/<name>`.
611 fn lock_device_path(device_path: &str) -> Result<DeviceLockGuard, TapeLockError> {
612
613 let lock_name = crate::tools::systemd::escape_unit(device_path, true);
614
615 let mut path = std::path::PathBuf::from(crate::tape::DRIVE_LOCK_DIR);
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)?;
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 }
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 }
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
640 let mut path = std::path::PathBuf::from(crate::tape::DRIVE_LOCK_DIR);
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 }