]> git.proxmox.com Git - proxmox-backup.git/blob - src/tape/drive/mod.rs
4abb95de034f78bd261bba0c21e750b9add84721
[proxmox-backup.git] / src / tape / drive / mod.rs
1 //! Tape drivers
2
3 mod virtual_tape;
4
5 pub mod linux_mtio;
6
7 mod tape_alert_flags;
8 pub use tape_alert_flags::*;
9
10 mod volume_statistics;
11 pub use volume_statistics::*;
12
13 mod encryption;
14 pub use encryption::*;
15
16 mod linux_tape;
17 pub use linux_tape::*;
18
19 mod mam;
20 pub use mam::*;
21
22 use std::os::unix::io::AsRawFd;
23
24 use anyhow::{bail, format_err, Error};
25 use ::serde::{Deserialize};
26 use serde_json::Value;
27
28 use proxmox::{
29 tools::{
30 Uuid,
31 io::ReadExt,
32 fs::{
33 fchown,
34 file_read_optional_string,
35 replace_file,
36 CreateOptions,
37 }
38 },
39 api::section_config::SectionConfigData,
40 };
41
42 use crate::{
43 task_log,
44 task::TaskState,
45 backup::{
46 Fingerprint,
47 KeyConfig,
48 },
49 api2::types::{
50 VirtualTapeDrive,
51 LinuxTapeDrive,
52 },
53 server::WorkerTask,
54 tape::{
55 TapeWrite,
56 TapeRead,
57 MediaId,
58 file_formats::{
59 PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0,
60 PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0,
61 MediaLabel,
62 MediaSetLabel,
63 MediaContentHeader,
64 },
65 changer::{
66 MediaChange,
67 MtxMediaChanger,
68 send_load_media_email,
69 },
70 },
71 };
72
73 /// Tape driver interface
74 pub trait TapeDriver {
75
76 /// Flush all data to the tape
77 fn sync(&mut self) -> Result<(), Error>;
78
79 /// Rewind the tape
80 fn rewind(&mut self) -> Result<(), Error>;
81
82 /// Move to end of recorded data
83 ///
84 /// We assume this flushes the tape write buffer.
85 fn move_to_eom(&mut self) -> 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 erase_media(&mut self, fast: bool) -> Result<(), Error>;
92
93 /// Read/Open the next file
94 fn read_next_file<'a>(&'a mut self) -> Result<Option<Box<dyn TapeRead + 'a>>, std::io::Error>;
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.rewind()?;
103
104 self.set_encryption(None)?;
105
106 self.erase_media(true)?;
107
108 let raw = serde_json::to_string_pretty(&serde_json::to_value(&label)?)?;
109
110 let header = MediaContentHeader::new(PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0, raw.len() as u32);
111
112 {
113 let mut writer = self.write_file()?;
114 writer.write_header(&header, raw.as_bytes())?;
115 writer.finish(false)?;
116 }
117
118 self.sync()?; // sync data to tape
119
120 Ok(())
121 }
122
123 /// Write the media set label to tape
124 ///
125 /// If the media-set is encrypted, we also store the encryption
126 /// key_config, so that it is possible to restore the key.
127 fn write_media_set_label(
128 &mut self,
129 media_set_label: &MediaSetLabel,
130 key_config: Option<&KeyConfig>,
131 ) -> Result<(), Error>;
132
133 /// Read the media label
134 ///
135 /// This tries to read both media labels (label and
136 /// media_set_label). Also returns the optional encryption key configuration.
137 fn read_label(&mut self) -> Result<(Option<MediaId>, Option<KeyConfig>), Error> {
138
139 self.rewind()?;
140
141 let label = {
142 let mut reader = match self.read_next_file()? {
143 None => return Ok((None, None)), // tape is empty
144 Some(reader) => reader,
145 };
146
147 let header: MediaContentHeader = unsafe { reader.read_le_value()? };
148 header.check(PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0, 1, 64*1024)?;
149 let data = reader.read_exact_allocated(header.size as usize)?;
150
151 let label: MediaLabel = serde_json::from_slice(&data)
152 .map_err(|err| format_err!("unable to parse drive label - {}", err))?;
153
154 // make sure we read the EOF marker
155 if reader.skip_to_end()? != 0 {
156 bail!("got unexpected data after label");
157 }
158
159 label
160 };
161
162 let mut media_id = MediaId { label, media_set_label: None };
163
164 // try to read MediaSet label
165 let mut reader = match self.read_next_file()? {
166 None => return Ok((Some(media_id), None)),
167 Some(reader) => reader,
168 };
169
170 let header: MediaContentHeader = unsafe { reader.read_le_value()? };
171 header.check(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, 1, 64*1024)?;
172 let data = reader.read_exact_allocated(header.size as usize)?;
173
174 let mut data: Value = serde_json::from_slice(&data)
175 .map_err(|err| format_err!("unable to parse media set label - {}", err))?;
176
177 let key_config_value = data["key-config"].take();
178 let key_config: Option<KeyConfig> = if !key_config_value.is_null() {
179 Some(serde_json::from_value(key_config_value)?)
180 } else {
181 None
182 };
183
184 let media_set_label: MediaSetLabel = serde_json::from_value(data)
185 .map_err(|err| format_err!("unable to parse media set label - {}", err))?;
186
187 // make sure we read the EOF marker
188 if reader.skip_to_end()? != 0 {
189 bail!("got unexpected data after media set label");
190 }
191
192 media_id.media_set_label = Some(media_set_label);
193
194 Ok((Some(media_id), key_config))
195 }
196
197 /// Eject media
198 fn eject_media(&mut self) -> Result<(), Error>;
199
200 /// Read Tape Alert Flags
201 ///
202 /// This make only sense for real LTO drives. Virtual tape drives should
203 /// simply return empty flags (default).
204 fn tape_alert_flags(&mut self) -> Result<TapeAlertFlags, Error> {
205 Ok(TapeAlertFlags::empty())
206 }
207
208 /// Set or clear encryption key
209 ///
210 /// We use the media_set_uuid to XOR the secret key with the
211 /// uuid (first 16 bytes), so that each media set uses an uique
212 /// key for encryption.
213 fn set_encryption(
214 &mut self,
215 key_fingerprint: Option<(Fingerprint, Uuid)>,
216 ) -> Result<(), Error> {
217 if key_fingerprint.is_some() {
218 bail!("drive does not support encryption");
219 }
220 Ok(())
221 }
222 }
223
224 /// Get the media changer (MediaChange + name) associated with a tape drive.
225 ///
226 /// Returns Ok(None) if the drive has no associated changer device.
227 ///
228 /// Note: This may return the drive name as changer-name if the drive
229 /// implements some kind of internal changer (which is true for our
230 /// 'virtual' drive implementation).
231 pub fn media_changer(
232 config: &SectionConfigData,
233 drive: &str,
234 ) -> Result<Option<(Box<dyn MediaChange>, String)>, Error> {
235
236 match config.sections.get(drive) {
237 Some((section_type_name, config)) => {
238 match section_type_name.as_ref() {
239 "virtual" => {
240 let tape = VirtualTapeDrive::deserialize(config)?;
241 Ok(Some((Box::new(tape), drive.to_string())))
242 }
243 "linux" => {
244 let drive_config = LinuxTapeDrive::deserialize(config)?;
245 match drive_config.changer {
246 Some(ref changer_name) => {
247 let changer = MtxMediaChanger::with_drive_config(&drive_config)?;
248 let changer_name = changer_name.to_string();
249 Ok(Some((Box::new(changer), changer_name)))
250 }
251 None => Ok(None),
252 }
253 }
254 _ => bail!("unknown drive type '{}' - internal error"),
255 }
256 }
257 None => {
258 bail!("no such drive '{}'", drive);
259 }
260 }
261 }
262
263 /// Get the media changer (MediaChange + name) associated with a tape drive.
264 ///
265 /// This fail if the drive has no associated changer device.
266 pub fn required_media_changer(
267 config: &SectionConfigData,
268 drive: &str,
269 ) -> Result<(Box<dyn MediaChange>, String), Error> {
270 match media_changer(config, drive) {
271 Ok(Some(result)) => {
272 Ok(result)
273 }
274 Ok(None) => {
275 bail!("drive '{}' has no associated changer device", drive);
276 },
277 Err(err) => {
278 Err(err)
279 }
280 }
281 }
282
283 /// Opens a tape drive (this fails if there is no media loaded)
284 pub fn open_drive(
285 config: &SectionConfigData,
286 drive: &str,
287 ) -> Result<Box<dyn TapeDriver>, Error> {
288
289 match config.sections.get(drive) {
290 Some((section_type_name, config)) => {
291 match section_type_name.as_ref() {
292 "virtual" => {
293 let tape = VirtualTapeDrive::deserialize(config)?;
294 let handle = tape.open()?;
295 Ok(Box::new(handle))
296 }
297 "linux" => {
298 let tape = LinuxTapeDrive::deserialize(config)?;
299 let handle = tape.open()?;
300 Ok(Box::new(handle))
301 }
302 _ => bail!("unknown drive type '{}' - internal error"),
303 }
304 }
305 None => {
306 bail!("no such drive '{}'", drive);
307 }
308 }
309 }
310
311 /// Requests a specific 'media' to be inserted into 'drive'. Within a
312 /// loop, this then tries to read the media label and waits until it
313 /// finds the requested media.
314 ///
315 /// Returns a handle to the opened drive and the media labels.
316 pub fn request_and_load_media(
317 worker: &WorkerTask,
318 config: &SectionConfigData,
319 drive: &str,
320 label: &MediaLabel,
321 ) -> Result<(
322 Box<dyn TapeDriver>,
323 MediaId,
324 ), Error> {
325
326 let check_label = |handle: &mut dyn TapeDriver, uuid: &proxmox::tools::Uuid| {
327 if let Ok((Some(media_id), _)) = handle.read_label() {
328 task_log!(
329 worker,
330 "found media label {} ({})",
331 media_id.label.label_text,
332 media_id.label.uuid,
333 );
334
335 if media_id.label.uuid == *uuid {
336 return Ok(media_id);
337 }
338 }
339 bail!("read label failed (please label all tapes first)");
340 };
341
342 match config.sections.get(drive) {
343 Some((section_type_name, config)) => {
344 match section_type_name.as_ref() {
345 "virtual" => {
346 let mut tape = VirtualTapeDrive::deserialize(config)?;
347
348 let label_text = label.label_text.clone();
349
350 tape.load_media(&label_text)?;
351
352 let mut handle: Box<dyn TapeDriver> = Box::new(tape.open()?);
353
354 let media_id = check_label(handle.as_mut(), &label.uuid)?;
355
356 Ok((handle, media_id))
357 }
358 "linux" => {
359 let drive_config = LinuxTapeDrive::deserialize(config)?;
360
361 let label_text = label.label_text.clone();
362
363 if drive_config.changer.is_some() {
364
365 let mut changer = MtxMediaChanger::with_drive_config(&drive_config)?;
366 changer.load_media(&label_text)?;
367
368 let mut handle: Box<dyn TapeDriver> = Box::new(drive_config.open()?);
369
370 let media_id = check_label(handle.as_mut(), &label.uuid)?;
371
372 return Ok((handle, media_id));
373 }
374
375 task_log!(worker, "Please insert media '{}' into drive '{}'", label_text, drive);
376
377 let to = "root@localhost"; // fixme
378
379 send_load_media_email(drive, &label_text, to)?;
380
381 let mut last_media_uuid = None;
382 let mut last_error = None;
383
384 loop {
385 worker.check_abort()?;
386
387 let mut handle = match drive_config.open() {
388 Ok(handle) => handle,
389 Err(err) => {
390 let err = err.to_string();
391 if Some(err.clone()) != last_error {
392 task_log!(worker, "tape open failed - {}", err);
393 last_error = Some(err);
394 }
395 for _ in 0..50 { // delay 5 seconds
396 worker.check_abort()?;
397 std::thread::sleep(std::time::Duration::from_millis(100));
398 }
399 continue;
400 }
401 };
402
403 match handle.read_label() {
404 Ok((Some(media_id), _)) => {
405 if media_id.label.uuid == label.uuid {
406 task_log!(
407 worker,
408 "found media label {} ({})",
409 media_id.label.label_text,
410 media_id.label.uuid.to_string(),
411 );
412 return Ok((Box::new(handle), media_id));
413 } else if Some(media_id.label.uuid.clone()) != last_media_uuid {
414 task_log!(
415 worker,
416 "wrong media label {} ({})",
417 media_id.label.label_text,
418 media_id.label.uuid.to_string(),
419 );
420 last_media_uuid = Some(media_id.label.uuid);
421 }
422 }
423 Ok((None, _)) => {
424 if last_media_uuid.is_some() {
425 task_log!(worker, "found empty media without label (please label all tapes first)");
426 last_media_uuid = None;
427 }
428 }
429 Err(err) => {
430 let err = err.to_string();
431 if Some(err.clone()) != last_error {
432 task_log!(worker, "tape open failed - {}", err);
433 last_error = Some(err);
434 }
435 }
436 }
437
438 // eprintln!("read label failed - test again in 5 secs");
439 for _ in 0..50 { // delay 5 seconds
440 worker.check_abort()?;
441 std::thread::sleep(std::time::Duration::from_millis(100));
442 }
443 }
444 }
445 _ => bail!("drive type '{}' not implemented!"),
446 }
447 }
448 None => {
449 bail!("no such drive '{}'", drive);
450 }
451 }
452 }
453
454 /// Aquires an exclusive lock for the tape device
455 ///
456 /// Basically calls lock_device_path() using the configured drive path.
457 pub fn lock_tape_device(
458 config: &SectionConfigData,
459 drive: &str,
460 ) -> Result<DeviceLockGuard, Error> {
461 let path = tape_device_path(config, drive)?;
462 lock_device_path(&path)
463 .map_err(|err| format_err!("unable to lock drive '{}' - {}", drive, err))
464 }
465
466 /// Writes the given state for the specified drive
467 ///
468 /// This function does not lock, so make sure the drive is locked
469 pub fn set_tape_device_state(
470 drive: &str,
471 state: &str,
472 ) -> Result<(), Error> {
473 let mut path = "/run/proxmox-backup/drive-state".to_string();
474 std::fs::create_dir_all(&path)?;
475 use std::fmt::Write;
476 write!(path, "/{}", drive)?;
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 }
487
488 /// Get the device state
489 pub 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
504 fn tape_device_path(
505 config: &SectionConfigData,
506 drive: &str,
507 ) -> Result<String, Error> {
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 };
519 Ok(path)
520 }
521 None => {
522 bail!("no such drive '{}'", drive);
523 }
524 }
525 }
526
527 pub 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>`.
533 fn 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 }
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
552 fn 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 }