]> git.proxmox.com Git - proxmox-backup.git/blob - src/tape/drive/mod.rs
update to first proxmox crate split
[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::path::PathBuf;
9
10 use anyhow::{bail, format_err, Error};
11 use serde::Deserialize;
12 use serde_json::Value;
13 use nix::fcntl::OFlag;
14 use nix::sys::stat::Mode;
15
16 use proxmox::{
17 tools::{
18 fs::{
19 lock_file,
20 atomic_open_or_create_file,
21 file_read_optional_string,
22 replace_file,
23 CreateOptions,
24 }
25 },
26 };
27 use proxmox_io::ReadExt;
28 use proxmox_section_config::SectionConfigData;
29 use proxmox_uuid::Uuid;
30
31 use pbs_api_types::{VirtualTapeDrive, LtoTapeDrive, Fingerprint};
32 use pbs_config::key_config::KeyConfig;
33 use pbs_tools::{task_log, task::WorkerTaskContext};
34
35 use pbs_tape::{
36 TapeWrite, TapeRead, BlockReadError, MediaContentHeader,
37 sg_tape::TapeAlertFlags,
38 };
39
40 use crate::{
41 server::send_load_media_email,
42 tape::{
43 MediaId,
44 drive::{
45 virtual_tape::open_virtual_tape_drive,
46 },
47 file_formats::{
48 PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0,
49 PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0,
50 MediaLabel,
51 MediaSetLabel,
52 },
53 changer::{
54 MediaChange,
55 MtxMediaChanger,
56 },
57 },
58 };
59
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 ///
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>;
75
76 /// Move to last file
77 fn move_to_last_file(&mut self) -> Result<(), Error>;
78
79 /// Move to given file nr
80 fn move_to_file(&mut self, file: u64) -> Result<(), Error>;
81
82 /// Current file number
83 fn current_file_number(&mut self) -> Result<u64, Error>;
84
85 /// Completely erase the media
86 fn format_media(&mut self, fast: bool) -> Result<(), Error>;
87
88 /// Read/Open the next file
89 fn read_next_file<'a>(&'a mut self) -> Result<Box<dyn TapeRead + 'a>, BlockReadError>;
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)
95 fn label_tape(&mut self, label: &MediaLabel) -> Result<(), Error> {
96
97 self.set_encryption(None)?;
98
99 self.format_media(true)?; // this rewinds the tape
100
101 let raw = serde_json::to_string_pretty(&serde_json::to_value(&label)?)?;
102
103 let header = MediaContentHeader::new(PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0, raw.len() as u32);
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
113 Ok(())
114 }
115
116 /// Write the media set label to tape
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>;
125
126 /// Read the media label
127 ///
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> {
131
132 self.rewind()?;
133
134 let label = {
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,
146 };
147
148 let header: MediaContentHeader = unsafe { reader.read_le_value()? };
149 header.check(PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0, 1, 64*1024)?;
150 let data = reader.read_exact_allocated(header.size as usize)?;
151
152 let label: MediaLabel = serde_json::from_slice(&data)
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
160 label
161 };
162
163 let mut media_id = MediaId { label, media_set_label: None };
164
165 // try to read MediaSet label
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,
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
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)
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
201 media_id.media_set_label = Some(media_set_label);
202
203 Ok((Some(media_id), key_config))
204 }
205
206 /// Eject media
207 fn eject_media(&mut self) -> Result<(), Error>;
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 }
216
217 /// Set or clear encryption key
218 ///
219 /// We use the media_set_uuid to XOR the secret key with the
220 /// uuid (first 16 bytes), so that each media set uses an unique
221 /// key for encryption.
222 fn set_encryption(
223 &mut self,
224 key_fingerprint: Option<(Fingerprint, Uuid)>,
225 ) -> Result<(), Error> {
226 if key_fingerprint.is_some() {
227 bail!("drive does not support encryption");
228 }
229 Ok(())
230 }
231 }
232
233 /// Get the media changer (MediaChange + name) associated with a tape drive.
234 ///
235 /// Returns Ok(None) if the drive has no associated changer device.
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).
240 pub fn media_changer(
241 config: &SectionConfigData,
242 drive: &str,
243 ) -> Result<Option<(Box<dyn MediaChange>, String)>, Error> {
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)?;
250 Ok(Some((Box::new(tape), drive.to_string())))
251 }
252 "lto" => {
253 let drive_config = LtoTapeDrive::deserialize(config)?;
254 match drive_config.changer {
255 Some(ref changer_name) => {
256 let changer = MtxMediaChanger::with_drive_config(&drive_config)?;
257 let changer_name = changer_name.to_string();
258 Ok(Some((Box::new(changer), changer_name)))
259 }
260 None => Ok(None),
261 }
262 }
263 _ => bail!("unknown drive type '{}' - internal error"),
264 }
265 }
266 None => {
267 bail!("no such drive '{}'", drive);
268 }
269 }
270 }
271
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)) => {
281 Ok(result)
282 }
283 Ok(None) => {
284 bail!("drive '{}' has no associated changer device", drive);
285 },
286 Err(err) => {
287 Err(err)
288 }
289 }
290 }
291
292 /// Opens a tape drive (this fails if there is no media loaded)
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)?;
303 let handle = open_virtual_tape_drive(&tape)?;
304 Ok(Box::new(handle))
305 }
306 "lto" => {
307 let tape = LtoTapeDrive::deserialize(config)?;
308 let handle = open_lto_tape_drive(&tape)?;
309 Ok(Box::new(handle))
310 }
311 _ => bail!("unknown drive type '{}' - internal error"),
312 }
313 }
314 None => {
315 bail!("no such drive '{}'", drive);
316 }
317 }
318 }
319
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
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(
357 worker: &dyn WorkerTaskContext,
358 config: &SectionConfigData,
359 drive: &str,
360 label: &MediaLabel,
361 notify_email: &Option<String>,
362 ) -> Result<(
363 Box<dyn TapeDriver>,
364 MediaId,
365 ), Error> {
366
367 let check_label = |handle: &mut dyn TapeDriver, uuid: &proxmox_uuid::Uuid| {
368 if let Ok((Some(media_id), _)) = handle.read_label() {
369 task_log!(
370 worker,
371 "found media label {} ({})",
372 media_id.label.label_text,
373 media_id.label.uuid,
374 );
375
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
383 match config.sections.get(drive) {
384 Some((section_type_name, config)) => {
385 match section_type_name.as_ref() {
386 "virtual" => {
387 let mut tape = VirtualTapeDrive::deserialize(config)?;
388
389 let label_text = label.label_text.clone();
390
391 tape.load_media(&label_text)?;
392
393 let mut handle: Box<dyn TapeDriver> = Box::new(open_virtual_tape_drive(&tape)?);
394
395 let media_id = check_label(handle.as_mut(), &label.uuid)?;
396
397 Ok((handle, media_id))
398 }
399 "lto" => {
400 let drive_config = LtoTapeDrive::deserialize(config)?;
401
402 let label_text = label.label_text.clone();
403
404 if drive_config.changer.is_some() {
405
406 task_log!(worker, "loading media '{}' into drive '{}'", label_text, drive);
407
408 let mut changer = MtxMediaChanger::with_drive_config(&drive_config)?;
409 changer.load_media(&label_text)?;
410
411 let mut handle: Box<dyn TapeDriver> = Box::new(open_lto_tape_drive(&drive_config)?);
412
413 let media_id = check_label(handle.as_mut(), &label.uuid)?;
414
415 return Ok((handle, media_id));
416 }
417
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 };
443
444 loop {
445 worker.check_abort()?;
446
447 if last_error != TapeRequestError::None {
448 for _ in 0..50 { // delay 5 seconds
449 worker.check_abort()?;
450 std::thread::sleep(std::time::Duration::from_millis(100));
451 }
452 } else {
453 task_log!(
454 worker,
455 "Checking for media '{}' in drive '{}'",
456 label_text,
457 drive
458 );
459 }
460
461 let mut handle = match open_lto_tape_drive(&drive_config) {
462 Ok(handle) => handle,
463 Err(err) => {
464 update_and_log_request_error(
465 &mut last_error,
466 TapeRequestError::OpenFailed(err.to_string()),
467 )?;
468 continue;
469 }
470 };
471
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 }
482 Ok((Some(media_id), _)) => {
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)
489 }
490 Ok((None, _)) => {
491 TapeRequestError::EmptyTape
492 }
493 Err(err) => {
494 TapeRequestError::ReadFailed(err.to_string())
495 }
496 };
497
498 update_and_log_request_error(&mut last_error, request_error)?;
499 }
500 }
501 _ => bail!("drive type '{}' not implemented!"),
502 }
503 }
504 None => {
505 bail!("no such drive '{}'", drive);
506 }
507 }
508 }
509
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
524 /// Acquires an exclusive lock for the tape device
525 ///
526 /// Basically calls lock_device_path() using the configured drive path.
527 pub fn lock_tape_device(
528 config: &SectionConfigData,
529 drive: &str,
530 ) -> Result<DeviceLockGuard, TapeLockError> {
531 let path = tape_device_path(config, drive)?;
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 })
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> {
547
548 let mut path = PathBuf::from(crate::tape::DRIVE_STATE_DIR);
549 path.push(drive);
550
551 let backup_user = pbs_config::backup_user()?;
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
558 replace_file(path, state.as_bytes(), options)
559 }
560
561 /// Get the device state
562 pub fn get_tape_device_state(
563 config: &SectionConfigData,
564 drive: &str,
565 ) -> Result<Option<String>, Error> {
566 let path = format!("{}/{}", crate::tape::DRIVE_STATE_DIR, drive);
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> {
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 }
587 "lto" => {
588 LtoTapeDrive::deserialize(config)?.path
589 }
590 _ => bail!("unknown drive type '{}' - internal error"),
591 };
592 Ok(path)
593 }
594 None => {
595 bail!("no such drive '{}'", drive);
596 }
597 }
598 }
599
600 pub struct DeviceLockGuard(std::fs::File);
601
602 // Uses systemd escape_unit to compute a file name from `device_path`, the try
603 // to lock `/var/lock/<name>`.
604 fn open_device_lock(device_path: &str) -> Result<std::fs::File, Error> {
605 let lock_name = proxmox::tools::systemd::escape_unit(device_path, true);
606
607 let mut path = std::path::PathBuf::from(crate::tape::DRIVE_LOCK_DIR);
608 path.push(lock_name);
609
610 let user = pbs_config::backup_user()?;
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,
621 )
622 }
623
624 // Acquires an exclusive lock on `device_path`
625 //
626 fn lock_device_path(device_path: &str) -> Result<DeviceLockGuard, TapeLockError> {
627 let mut file = open_device_lock(device_path)?;
628 let timeout = std::time::Duration::new(10, 0);
629 if let Err(err) = lock_file(&mut file, true, Some(timeout)) {
630 if err.kind() == std::io::ErrorKind::Interrupted {
631 return Err(TapeLockError::TimeOut);
632 } else {
633 return Err(err.into());
634 }
635 }
636
637 Ok(DeviceLockGuard(file))
638 }
639
640 // Same logic as lock_device_path, but uses a timeout of 0, making it
641 // non-blocking, and returning if the file is locked or not
642 fn test_device_path_lock(device_path: &str) -> Result<bool, Error> {
643
644 let mut file = open_device_lock(device_path)?;
645
646 let timeout = std::time::Duration::new(0, 0);
647 match lock_file(&mut file, true, Some(timeout)) {
648 // file was not locked, continue
649 Ok(()) => {},
650 // file was locked, return true
651 Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => return Ok(true),
652 Err(err) => bail!("{}", err),
653 }
654
655 Ok(false)
656 }