2020-12-07 07:27:15 +00:00
|
|
|
mod virtual_tape;
|
|
|
|
mod linux_mtio;
|
2021-01-02 09:55:30 +00:00
|
|
|
|
2021-01-03 09:09:43 +00:00
|
|
|
mod tape_alert_flags;
|
|
|
|
pub use tape_alert_flags::*;
|
|
|
|
|
2021-01-05 11:58:18 +00:00
|
|
|
mod volume_statistics;
|
|
|
|
pub use volume_statistics::*;
|
|
|
|
|
2020-12-22 09:42:22 +00:00
|
|
|
pub mod linux_tape;
|
2020-12-07 07:27:15 +00:00
|
|
|
|
2020-12-21 11:11:52 +00:00
|
|
|
mod mam;
|
|
|
|
pub use mam::*;
|
|
|
|
|
2020-12-07 07:27:15 +00:00
|
|
|
mod linux_list_drives;
|
|
|
|
pub use linux_list_drives::*;
|
|
|
|
|
|
|
|
use anyhow::{bail, format_err, Error};
|
2020-12-16 12:27:53 +00:00
|
|
|
use ::serde::{Deserialize};
|
2020-12-07 07:27:15 +00:00
|
|
|
|
|
|
|
use proxmox::tools::io::ReadExt;
|
|
|
|
use proxmox::api::section_config::SectionConfigData;
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
api2::types::{
|
|
|
|
VirtualTapeDrive,
|
|
|
|
LinuxTapeDrive,
|
|
|
|
},
|
2020-12-30 11:58:06 +00:00
|
|
|
server::WorkerTask,
|
2020-12-07 07:27:15 +00:00
|
|
|
tape::{
|
|
|
|
TapeWrite,
|
|
|
|
TapeRead,
|
2020-12-16 12:27:53 +00:00
|
|
|
MediaId,
|
2021-01-10 09:02:01 +00:00
|
|
|
MtxMediaChanger,
|
2020-12-07 07:27:15 +00:00
|
|
|
file_formats::{
|
2020-12-14 16:37:16 +00:00
|
|
|
PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0,
|
2020-12-07 07:27:15 +00:00
|
|
|
PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0,
|
2020-12-14 16:37:16 +00:00
|
|
|
MediaLabel,
|
2020-12-07 07:27:15 +00:00
|
|
|
MediaSetLabel,
|
|
|
|
MediaContentHeader,
|
|
|
|
},
|
|
|
|
changer::{
|
|
|
|
MediaChange,
|
2020-12-30 16:16:57 +00:00
|
|
|
send_load_media_email,
|
2020-12-07 07:27:15 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
/// Tape driver interface
|
|
|
|
pub trait TapeDriver {
|
|
|
|
|
|
|
|
/// Flush all data to the tape
|
|
|
|
fn sync(&mut self) -> Result<(), Error>;
|
|
|
|
|
|
|
|
/// Rewind the tape
|
|
|
|
fn rewind(&mut self) -> Result<(), Error>;
|
|
|
|
|
|
|
|
/// Move to end of recorded data
|
|
|
|
///
|
|
|
|
/// We assume this flushes the tape write buffer.
|
|
|
|
fn move_to_eom(&mut self) -> Result<(), Error>;
|
|
|
|
|
|
|
|
/// Current file number
|
2020-12-18 06:44:50 +00:00
|
|
|
fn current_file_number(&mut self) -> Result<u64, Error>;
|
2020-12-07 07:27:15 +00:00
|
|
|
|
|
|
|
/// Completely erase the media
|
|
|
|
fn erase_media(&mut self, fast: bool) -> Result<(), Error>;
|
|
|
|
|
|
|
|
/// Read/Open the next file
|
|
|
|
fn read_next_file<'a>(&'a mut self) -> Result<Option<Box<dyn TapeRead + 'a>>, std::io::Error>;
|
|
|
|
|
|
|
|
/// Write/Append a new file
|
|
|
|
fn write_file<'a>(&'a mut self) -> Result<Box<dyn TapeWrite + 'a>, std::io::Error>;
|
|
|
|
|
|
|
|
/// Write label to tape (erase tape content)
|
2020-12-16 12:27:53 +00:00
|
|
|
fn label_tape(&mut self, label: &MediaLabel) -> Result<(), Error> {
|
2020-12-07 07:27:15 +00:00
|
|
|
|
|
|
|
self.rewind()?;
|
|
|
|
|
|
|
|
self.erase_media(true)?;
|
|
|
|
|
|
|
|
let raw = serde_json::to_string_pretty(&serde_json::to_value(&label)?)?;
|
|
|
|
|
2020-12-14 16:37:16 +00:00
|
|
|
let header = MediaContentHeader::new(PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0, raw.len() as u32);
|
2020-12-07 07:27:15 +00:00
|
|
|
|
|
|
|
{
|
|
|
|
let mut writer = self.write_file()?;
|
|
|
|
writer.write_header(&header, raw.as_bytes())?;
|
|
|
|
writer.finish(false)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
self.sync()?; // sync data to tape
|
|
|
|
|
2020-12-16 12:27:53 +00:00
|
|
|
Ok(())
|
2020-12-07 07:27:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Write the media set label to tape
|
2020-12-16 12:27:53 +00:00
|
|
|
fn write_media_set_label(&mut self, media_set_label: &MediaSetLabel) -> Result<(), Error>;
|
2020-12-07 07:27:15 +00:00
|
|
|
|
|
|
|
/// Read the media label
|
|
|
|
///
|
|
|
|
/// This tries to read both media labels (label and media_set_label).
|
2020-12-16 12:27:53 +00:00
|
|
|
fn read_label(&mut self) -> Result<Option<MediaId>, Error> {
|
2020-12-07 07:27:15 +00:00
|
|
|
|
|
|
|
self.rewind()?;
|
|
|
|
|
2020-12-16 12:27:53 +00:00
|
|
|
let label = {
|
2020-12-07 07:27:15 +00:00
|
|
|
let mut reader = match self.read_next_file()? {
|
|
|
|
None => return Ok(None), // tape is empty
|
|
|
|
Some(reader) => reader,
|
|
|
|
};
|
|
|
|
|
|
|
|
let header: MediaContentHeader = unsafe { reader.read_le_value()? };
|
2020-12-14 16:37:16 +00:00
|
|
|
header.check(PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0, 1, 64*1024)?;
|
2020-12-07 07:27:15 +00:00
|
|
|
let data = reader.read_exact_allocated(header.size as usize)?;
|
|
|
|
|
2020-12-14 16:37:16 +00:00
|
|
|
let label: MediaLabel = serde_json::from_slice(&data)
|
2020-12-07 07:27:15 +00:00
|
|
|
.map_err(|err| format_err!("unable to parse drive label - {}", err))?;
|
|
|
|
|
|
|
|
// make sure we read the EOF marker
|
|
|
|
if reader.skip_to_end()? != 0 {
|
|
|
|
bail!("got unexpected data after label");
|
|
|
|
}
|
|
|
|
|
2020-12-16 12:27:53 +00:00
|
|
|
label
|
2020-12-07 07:27:15 +00:00
|
|
|
};
|
|
|
|
|
2020-12-16 12:27:53 +00:00
|
|
|
let mut media_id = MediaId { label, media_set_label: None };
|
2020-12-07 07:27:15 +00:00
|
|
|
|
2020-12-16 12:27:53 +00:00
|
|
|
// try to read MediaSet label
|
2020-12-07 07:27:15 +00:00
|
|
|
let mut reader = match self.read_next_file()? {
|
2020-12-16 12:27:53 +00:00
|
|
|
None => return Ok(Some(media_id)),
|
2020-12-07 07:27:15 +00:00
|
|
|
Some(reader) => reader,
|
|
|
|
};
|
|
|
|
|
|
|
|
let header: MediaContentHeader = unsafe { reader.read_le_value()? };
|
|
|
|
header.check(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, 1, 64*1024)?;
|
|
|
|
let data = reader.read_exact_allocated(header.size as usize)?;
|
|
|
|
|
|
|
|
let media_set_label: MediaSetLabel = serde_json::from_slice(&data)
|
|
|
|
.map_err(|err| format_err!("unable to parse media set label - {}", err))?;
|
|
|
|
|
|
|
|
// make sure we read the EOF marker
|
|
|
|
if reader.skip_to_end()? != 0 {
|
|
|
|
bail!("got unexpected data after media set label");
|
|
|
|
}
|
|
|
|
|
2020-12-16 12:27:53 +00:00
|
|
|
media_id.media_set_label = Some(media_set_label);
|
2020-12-07 07:27:15 +00:00
|
|
|
|
2020-12-16 12:27:53 +00:00
|
|
|
Ok(Some(media_id))
|
2020-12-07 07:27:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Eject media
|
|
|
|
fn eject_media(&mut self) -> Result<(), Error>;
|
2021-01-09 11:34:00 +00:00
|
|
|
|
|
|
|
/// Read Tape Alert Flags
|
|
|
|
///
|
|
|
|
/// This make only sense for real LTO drives. Virtual tape drives should
|
|
|
|
/// simply return empty flags (default).
|
|
|
|
fn tape_alert_flags(&mut self) -> Result<TapeAlertFlags, Error> {
|
|
|
|
Ok(TapeAlertFlags::empty())
|
|
|
|
}
|
2020-12-07 07:27:15 +00:00
|
|
|
}
|
|
|
|
|
2020-12-30 16:16:57 +00:00
|
|
|
/// Get the media changer (MediaChange + name) associated with a tape drive.
|
2020-12-07 07:27:15 +00:00
|
|
|
///
|
2020-12-30 16:16:57 +00:00
|
|
|
/// Returns Ok(None) if the drive has no associated changer device.
|
2020-12-30 16:28:33 +00:00
|
|
|
///
|
|
|
|
/// Note: This may return the drive name as changer-name if the drive
|
|
|
|
/// implements some kind of internal changer (which is true for our
|
|
|
|
/// 'virtual' drive implementation).
|
2020-12-07 07:27:15 +00:00
|
|
|
pub fn media_changer(
|
|
|
|
config: &SectionConfigData,
|
|
|
|
drive: &str,
|
2020-12-30 16:16:57 +00:00
|
|
|
) -> Result<Option<(Box<dyn MediaChange>, String)>, Error> {
|
2020-12-07 07:27:15 +00:00
|
|
|
|
|
|
|
match config.sections.get(drive) {
|
|
|
|
Some((section_type_name, config)) => {
|
|
|
|
match section_type_name.as_ref() {
|
|
|
|
"virtual" => {
|
|
|
|
let tape = VirtualTapeDrive::deserialize(config)?;
|
2020-12-30 16:16:57 +00:00
|
|
|
Ok(Some((Box::new(tape), drive.to_string())))
|
2020-12-07 07:27:15 +00:00
|
|
|
}
|
|
|
|
"linux" => {
|
2021-01-10 09:02:01 +00:00
|
|
|
let drive_config = LinuxTapeDrive::deserialize(config)?;
|
|
|
|
match drive_config.changer {
|
2020-12-07 07:27:15 +00:00
|
|
|
Some(ref changer_name) => {
|
2021-01-10 09:02:01 +00:00
|
|
|
let changer = MtxMediaChanger::with_drive_config(&drive_config)?;
|
2020-12-07 07:27:15 +00:00
|
|
|
let changer_name = changer_name.to_string();
|
2021-01-10 09:02:01 +00:00
|
|
|
Ok(Some((Box::new(changer), changer_name)))
|
2020-12-07 07:27:15 +00:00
|
|
|
}
|
2020-12-30 16:16:57 +00:00
|
|
|
None => Ok(None),
|
2020-12-07 07:27:15 +00:00
|
|
|
}
|
|
|
|
}
|
2020-12-30 16:16:57 +00:00
|
|
|
_ => bail!("unknown drive type '{}' - internal error"),
|
2020-12-07 07:27:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
None => {
|
|
|
|
bail!("no such drive '{}'", drive);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-30 16:16:57 +00:00
|
|
|
/// Get the media changer (MediaChange + name) associated with a tape drive.
|
|
|
|
///
|
|
|
|
/// This fail if the drive has no associated changer device.
|
|
|
|
pub fn required_media_changer(
|
|
|
|
config: &SectionConfigData,
|
|
|
|
drive: &str,
|
|
|
|
) -> Result<(Box<dyn MediaChange>, String), Error> {
|
|
|
|
match media_changer(config, drive) {
|
|
|
|
Ok(Some(result)) => {
|
|
|
|
return Ok(result);
|
|
|
|
}
|
|
|
|
Ok(None) => {
|
|
|
|
bail!("drive '{}' has no associated changer device", drive);
|
|
|
|
},
|
|
|
|
Err(err) => {
|
|
|
|
return Err(err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-07 07:27:15 +00:00
|
|
|
pub fn open_drive(
|
|
|
|
config: &SectionConfigData,
|
|
|
|
drive: &str,
|
|
|
|
) -> Result<Box<dyn TapeDriver>, Error> {
|
|
|
|
|
|
|
|
match config.sections.get(drive) {
|
|
|
|
Some((section_type_name, config)) => {
|
|
|
|
match section_type_name.as_ref() {
|
|
|
|
"virtual" => {
|
|
|
|
let tape = VirtualTapeDrive::deserialize(config)?;
|
|
|
|
let handle = tape.open()
|
|
|
|
.map_err(|err| format_err!("open drive '{}' ({}) failed - {}", drive, tape.path, err))?;
|
|
|
|
Ok(Box::new(handle))
|
|
|
|
}
|
|
|
|
"linux" => {
|
|
|
|
let tape = LinuxTapeDrive::deserialize(config)?;
|
|
|
|
let handle = tape.open()
|
|
|
|
.map_err(|err| format_err!("open drive '{}' ({}) failed - {}", drive, tape.path, err))?;
|
|
|
|
Ok(Box::new(handle))
|
|
|
|
}
|
2020-12-30 16:16:57 +00:00
|
|
|
_ => bail!("unknown drive type '{}' - internal error"),
|
2020-12-07 07:27:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
None => {
|
|
|
|
bail!("no such drive '{}'", drive);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Requests a specific 'media' to be inserted into 'drive'. Within a
|
|
|
|
/// loop, this then tries to read the media label and waits until it
|
|
|
|
/// finds the requested media.
|
|
|
|
///
|
|
|
|
/// Returns a handle to the opened drive and the media labels.
|
|
|
|
pub fn request_and_load_media(
|
2020-12-30 11:58:06 +00:00
|
|
|
worker: &WorkerTask,
|
2020-12-07 07:27:15 +00:00
|
|
|
config: &SectionConfigData,
|
|
|
|
drive: &str,
|
2020-12-14 16:37:16 +00:00
|
|
|
label: &MediaLabel,
|
2020-12-07 07:27:15 +00:00
|
|
|
) -> Result<(
|
|
|
|
Box<dyn TapeDriver>,
|
2020-12-16 12:27:53 +00:00
|
|
|
MediaId,
|
2020-12-07 07:27:15 +00:00
|
|
|
), Error> {
|
|
|
|
|
2020-12-30 11:58:06 +00:00
|
|
|
let check_label = |handle: &mut dyn TapeDriver, uuid: &proxmox::tools::Uuid| {
|
|
|
|
if let Ok(Some(media_id)) = handle.read_label() {
|
2020-12-30 16:16:57 +00:00
|
|
|
worker.log(format!(
|
|
|
|
"found media label {} ({})",
|
2021-01-13 12:26:59 +00:00
|
|
|
media_id.label.label_text,
|
2020-12-30 16:16:57 +00:00
|
|
|
media_id.label.uuid.to_string(),
|
|
|
|
));
|
2020-12-30 11:58:06 +00:00
|
|
|
if media_id.label.uuid == *uuid {
|
|
|
|
return Ok(media_id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
bail!("read label failed (please label all tapes first)");
|
|
|
|
};
|
|
|
|
|
2020-12-07 07:27:15 +00:00
|
|
|
match config.sections.get(drive) {
|
|
|
|
Some((section_type_name, config)) => {
|
|
|
|
match section_type_name.as_ref() {
|
|
|
|
"virtual" => {
|
2020-12-30 11:58:06 +00:00
|
|
|
let mut tape = VirtualTapeDrive::deserialize(config)?;
|
2020-12-07 07:27:15 +00:00
|
|
|
|
2021-01-13 12:26:59 +00:00
|
|
|
let label_text = label.label_text.clone();
|
2020-12-07 07:27:15 +00:00
|
|
|
|
2021-01-13 12:26:59 +00:00
|
|
|
tape.load_media(&label_text)?;
|
2020-12-07 07:27:15 +00:00
|
|
|
|
2020-12-30 11:58:06 +00:00
|
|
|
let mut handle: Box<dyn TapeDriver> = Box::new(tape.open()?);
|
2020-12-07 07:27:15 +00:00
|
|
|
|
2020-12-30 11:58:06 +00:00
|
|
|
let media_id = check_label(handle.as_mut(), &label.uuid)?;
|
|
|
|
|
|
|
|
return Ok((handle, media_id));
|
2020-12-07 07:27:15 +00:00
|
|
|
}
|
|
|
|
"linux" => {
|
2021-01-10 09:02:01 +00:00
|
|
|
let drive_config = LinuxTapeDrive::deserialize(config)?;
|
2020-12-30 11:58:06 +00:00
|
|
|
|
2021-01-13 12:26:59 +00:00
|
|
|
let label_text = label.label_text.clone();
|
2020-12-07 07:27:15 +00:00
|
|
|
|
2021-01-10 09:02:01 +00:00
|
|
|
if drive_config.changer.is_some() {
|
2020-12-07 07:27:15 +00:00
|
|
|
|
2021-01-10 09:02:01 +00:00
|
|
|
let mut changer = MtxMediaChanger::with_drive_config(&drive_config)?;
|
2021-01-13 12:26:59 +00:00
|
|
|
changer.load_media(&label_text)?;
|
2020-12-30 11:58:06 +00:00
|
|
|
|
2021-01-10 09:02:01 +00:00
|
|
|
let mut handle: Box<dyn TapeDriver> = Box::new(drive_config.open()?);
|
2020-12-30 11:58:06 +00:00
|
|
|
|
|
|
|
let media_id = check_label(handle.as_mut(), &label.uuid)?;
|
|
|
|
|
|
|
|
return Ok((handle, media_id));
|
|
|
|
}
|
|
|
|
|
2021-01-13 12:26:59 +00:00
|
|
|
worker.log(format!("Please insert media '{}' into drive '{}'", label_text, drive));
|
2020-12-30 11:58:06 +00:00
|
|
|
|
|
|
|
let to = "root@localhost"; // fixme
|
|
|
|
|
2021-01-13 12:26:59 +00:00
|
|
|
send_load_media_email(drive, &label_text, to)?;
|
2020-12-30 11:58:06 +00:00
|
|
|
|
|
|
|
let mut last_media_uuid = None;
|
2020-12-30 18:17:18 +00:00
|
|
|
let mut last_error = None;
|
2020-12-07 07:27:15 +00:00
|
|
|
|
|
|
|
loop {
|
2021-01-10 09:02:01 +00:00
|
|
|
let mut handle = match drive_config.open() {
|
2020-12-07 07:27:15 +00:00
|
|
|
Ok(handle) => handle,
|
2020-12-30 18:17:18 +00:00
|
|
|
Err(err) => {
|
|
|
|
let err = err.to_string();
|
|
|
|
if Some(err.clone()) != last_error {
|
|
|
|
worker.log(format!("tape open failed - {}", err));
|
|
|
|
last_error = Some(err);
|
|
|
|
}
|
2020-12-07 07:27:15 +00:00
|
|
|
std::thread::sleep(std::time::Duration::from_millis(5_000));
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-12-30 11:58:06 +00:00
|
|
|
match handle.read_label() {
|
|
|
|
Ok(Some(media_id)) => {
|
|
|
|
if media_id.label.uuid == label.uuid {
|
|
|
|
worker.log(format!(
|
|
|
|
"found media label {} ({})",
|
2021-01-13 12:26:59 +00:00
|
|
|
media_id.label.label_text,
|
2020-12-30 11:58:06 +00:00
|
|
|
media_id.label.uuid.to_string(),
|
|
|
|
));
|
|
|
|
return Ok((Box::new(handle), media_id));
|
|
|
|
} else {
|
|
|
|
if Some(media_id.label.uuid.clone()) != last_media_uuid {
|
|
|
|
worker.log(format!(
|
|
|
|
"wrong media label {} ({})",
|
2021-01-13 12:26:59 +00:00
|
|
|
media_id.label.label_text,
|
2020-12-30 11:58:06 +00:00
|
|
|
media_id.label.uuid.to_string(),
|
|
|
|
));
|
|
|
|
last_media_uuid = Some(media_id.label.uuid);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(None) => {
|
|
|
|
if last_media_uuid.is_some() {
|
|
|
|
worker.log(format!("found empty media without label (please label all tapes first)"));
|
|
|
|
last_media_uuid = None;
|
|
|
|
}
|
2020-12-07 07:27:15 +00:00
|
|
|
}
|
2020-12-30 18:17:18 +00:00
|
|
|
Err(err) => {
|
|
|
|
let err = err.to_string();
|
|
|
|
if Some(err.clone()) != last_error {
|
|
|
|
worker.log(format!("tape open failed - {}", err));
|
|
|
|
last_error = Some(err);
|
|
|
|
}
|
|
|
|
}
|
2020-12-07 07:27:15 +00:00
|
|
|
}
|
|
|
|
|
2020-12-30 11:58:06 +00:00
|
|
|
// eprintln!("read label failed - test again in 5 secs");
|
2020-12-07 07:27:15 +00:00
|
|
|
std::thread::sleep(std::time::Duration::from_millis(5_000));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => bail!("drive type '{}' not implemented!"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None => {
|
|
|
|
bail!("no such drive '{}'", drive);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|