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