]> git.proxmox.com Git - proxmox-backup.git/blame - src/tape/drive/mod.rs
tape: create tmp dirs early at server startup
[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
6fe16039 366 let mut changer = MtxMediaChanger::with_drive_config(&drive_config)?;
8446fbca 367 changer.load_media(&label_text)?;
ff58c519 368
6fe16039 369 let mut handle: Box<dyn TapeDriver> = Box::new(drive_config.open()?);
ff58c519
DM
370
371 let media_id = check_label(handle.as_mut(), &label.uuid)?;
372
373 return Ok((handle, media_id));
374 }
375
271764de 376 task_log!(worker, "Please insert media '{}' into drive '{}'", label_text, drive);
ff58c519
DM
377
378 let to = "root@localhost"; // fixme
ff58c519 379
8446fbca 380 send_load_media_email(drive, &label_text, to)?;
ff58c519
DM
381
382 let mut last_media_uuid = None;
81764111 383 let mut last_error = None;
fa9c9be7
DM
384
385 loop {
271764de
DM
386 worker.check_abort()?;
387
6fe16039 388 let mut handle = match drive_config.open() {
fa9c9be7 389 Ok(handle) => handle,
81764111
DM
390 Err(err) => {
391 let err = err.to_string();
392 if Some(err.clone()) != last_error {
271764de 393 task_log!(worker, "tape open failed - {}", err);
81764111
DM
394 last_error = Some(err);
395 }
271764de
DM
396 for _ in 0..50 { // delay 5 seconds
397 worker.check_abort()?;
398 std::thread::sleep(std::time::Duration::from_millis(100));
399 }
fa9c9be7
DM
400 continue;
401 }
402 };
403
ff58c519 404 match handle.read_label() {
feb1645f 405 Ok((Some(media_id), _)) => {
ff58c519 406 if media_id.label.uuid == label.uuid {
271764de
DM
407 task_log!(
408 worker,
ff58c519 409 "found media label {} ({})",
8446fbca 410 media_id.label.label_text,
ff58c519 411 media_id.label.uuid.to_string(),
271764de 412 );
ff58c519 413 return Ok((Box::new(handle), media_id));
6334bdc1 414 } else if Some(media_id.label.uuid.clone()) != last_media_uuid {
271764de
DM
415 task_log!(
416 worker,
6334bdc1
FG
417 "wrong media label {} ({})",
418 media_id.label.label_text,
419 media_id.label.uuid.to_string(),
271764de 420 );
6334bdc1 421 last_media_uuid = Some(media_id.label.uuid);
ff58c519
DM
422 }
423 }
feb1645f 424 Ok((None, _)) => {
ff58c519 425 if last_media_uuid.is_some() {
271764de 426 task_log!(worker, "found empty media without label (please label all tapes first)");
ff58c519
DM
427 last_media_uuid = None;
428 }
fa9c9be7 429 }
81764111
DM
430 Err(err) => {
431 let err = err.to_string();
432 if Some(err.clone()) != last_error {
271764de 433 task_log!(worker, "tape open failed - {}", err);
81764111
DM
434 last_error = Some(err);
435 }
436 }
fa9c9be7
DM
437 }
438
ff58c519 439 // eprintln!("read label failed - test again in 5 secs");
271764de
DM
440 for _ in 0..50 { // delay 5 seconds
441 worker.check_abort()?;
442 std::thread::sleep(std::time::Duration::from_millis(100));
443 }
fa9c9be7
DM
444 }
445 }
446 _ => bail!("drive type '{}' not implemented!"),
447 }
448 }
449 None => {
450 bail!("no such drive '{}'", drive);
451 }
452 }
453}
25aa55b5
DM
454
455/// Aquires an exclusive lock for the tape device
456///
457/// Basically calls lock_device_path() using the configured drive path.
458pub fn lock_tape_device(
459 config: &SectionConfigData,
460 drive: &str,
461) -> Result<DeviceLockGuard, Error> {
546d2653
DC
462 let path = tape_device_path(config, drive)?;
463 lock_device_path(&path)
464 .map_err(|err| format_err!("unable to lock drive '{}' - {}", drive, err))
465}
466
467/// Writes the given state for the specified drive
468///
469/// This function does not lock, so make sure the drive is locked
470pub fn set_tape_device_state(
471 drive: &str,
472 state: &str,
473) -> Result<(), Error> {
cd44fb8d
DM
474
475 let mut path = PathBuf::from(crate::tape::DRIVE_STATE_DIR);
476 path.push(drive);
546d2653
DC
477
478 let backup_user = crate::backup::backup_user()?;
479 let mode = nix::sys::stat::Mode::from_bits_truncate(0o0644);
480 let options = CreateOptions::new()
481 .perm(mode)
482 .owner(backup_user.uid)
483 .group(backup_user.gid);
484
485 replace_file(path, state.as_bytes(), options)
486}
25aa55b5 487
546d2653
DC
488/// Get the device state
489pub fn get_tape_device_state(
490 config: &SectionConfigData,
491 drive: &str,
492) -> Result<Option<String>, Error> {
493 let path = format!("/run/proxmox-backup/drive-state/{}", drive);
494 let state = file_read_optional_string(path)?;
495
496 let device_path = tape_device_path(config, drive)?;
497 if test_device_path_lock(&device_path)? {
498 Ok(state)
499 } else {
500 Ok(None)
501 }
502}
503
504fn tape_device_path(
505 config: &SectionConfigData,
506 drive: &str,
507) -> Result<String, Error> {
25aa55b5
DM
508 match config.sections.get(drive) {
509 Some((section_type_name, config)) => {
510 let path = match section_type_name.as_ref() {
511 "virtual" => {
512 VirtualTapeDrive::deserialize(config)?.path
513 }
514 "linux" => {
515 LinuxTapeDrive::deserialize(config)?.path
516 }
517 _ => bail!("unknown drive type '{}' - internal error"),
518 };
546d2653 519 Ok(path)
25aa55b5
DM
520 }
521 None => {
522 bail!("no such drive '{}'", drive);
523 }
524 }
525}
526
527pub struct DeviceLockGuard(std::fs::File);
528
529// Aquires an exclusive lock on `device_path`
530//
531// Uses systemd escape_unit to compute a file name from `device_path`, the try
532// to lock `/var/lock/<name>`.
533fn lock_device_path(device_path: &str) -> Result<DeviceLockGuard, Error> {
534
535 let lock_name = crate::tools::systemd::escape_unit(device_path, true);
536
537 let mut path = std::path::PathBuf::from("/var/lock");
538 path.push(lock_name);
539
540 let timeout = std::time::Duration::new(10, 0);
541 let mut file = std::fs::OpenOptions::new().create(true).append(true).open(path)?;
542 proxmox::tools::fs::lock_file(&mut file, true, Some(timeout))?;
543
544 let backup_user = crate::backup::backup_user()?;
545 fchown(file.as_raw_fd(), Some(backup_user.uid), Some(backup_user.gid))?;
546
547 Ok(DeviceLockGuard(file))
548}
33c06b33
DC
549
550// Same logic as lock_device_path, but uses a timeout of 0, making it
551// non-blocking, and returning if the file is locked or not
552fn test_device_path_lock(device_path: &str) -> Result<bool, Error> {
553
554 let lock_name = crate::tools::systemd::escape_unit(device_path, true);
555
556 let mut path = std::path::PathBuf::from("/var/lock");
557 path.push(lock_name);
558
559 let timeout = std::time::Duration::new(0, 0);
560 let mut file = std::fs::OpenOptions::new().create(true).append(true).open(path)?;
561 match proxmox::tools::fs::lock_file(&mut file, true, Some(timeout)) {
562 // file was not locked, continue
563 Ok(()) => {},
564 // file was locked, return true
565 Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => return Ok(true),
566 Err(err) => bail!("{}", err),
567 }
568
569 let backup_user = crate::backup::backup_user()?;
570 fchown(file.as_raw_fd(), Some(backup_user.uid), Some(backup_user.gid))?;
571
572 Ok(false)
573}