d1d74c4367
found and semi-manually replaced by using: codespell -L mut -L crate -i 3 -w Mostly in comments, but also email notification and two occurrences of misspelled 'reserved' struct member, which where not used and cargo build did not complain about the change, soo ... Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
654 lines
20 KiB
Rust
654 lines
20 KiB
Rust
//! Media Pool
|
|
//!
|
|
//! A set of backup medias.
|
|
//!
|
|
//! This struct manages backup media state during backup. The main
|
|
//! purpose is to allocate media sets and assign new tapes to it.
|
|
//!
|
|
//!
|
|
|
|
use std::path::{PathBuf, Path};
|
|
use anyhow::{bail, Error};
|
|
use ::serde::{Deserialize, Serialize};
|
|
|
|
use proxmox::tools::Uuid;
|
|
|
|
use crate::{
|
|
backup::Fingerprint,
|
|
api2::types::{
|
|
MediaStatus,
|
|
MediaLocation,
|
|
MediaSetPolicy,
|
|
RetentionPolicy,
|
|
MediaPoolConfig,
|
|
},
|
|
tools::systemd::time::compute_next_event,
|
|
tape::{
|
|
MediaId,
|
|
MediaSet,
|
|
Inventory,
|
|
file_formats::{
|
|
MediaLabel,
|
|
MediaSetLabel,
|
|
},
|
|
}
|
|
};
|
|
|
|
/// Media Pool lock guard
|
|
pub struct MediaPoolLockGuard(std::fs::File);
|
|
|
|
/// Media Pool
|
|
pub struct MediaPool {
|
|
|
|
name: String,
|
|
state_path: PathBuf,
|
|
|
|
media_set_policy: MediaSetPolicy,
|
|
retention: RetentionPolicy,
|
|
|
|
changer_name: Option<String>,
|
|
force_media_availability: bool,
|
|
|
|
encrypt_fingerprint: Option<Fingerprint>,
|
|
|
|
inventory: Inventory,
|
|
|
|
current_media_set: MediaSet,
|
|
}
|
|
|
|
impl MediaPool {
|
|
|
|
/// Creates a new instance
|
|
///
|
|
/// If you specify a `changer_name`, only media accessible via
|
|
/// that changer is considered available. If you pass `None` for
|
|
/// `changer`, all offline media is considered available (backups
|
|
/// to standalone drives may not use media from inside a tape
|
|
/// library).
|
|
pub fn new(
|
|
name: &str,
|
|
state_path: &Path,
|
|
media_set_policy: MediaSetPolicy,
|
|
retention: RetentionPolicy,
|
|
changer_name: Option<String>,
|
|
encrypt_fingerprint: Option<Fingerprint>,
|
|
) -> Result<Self, Error> {
|
|
|
|
let inventory = Inventory::load(state_path)?;
|
|
|
|
let current_media_set = match inventory.latest_media_set(name) {
|
|
Some(set_uuid) => inventory.compute_media_set_members(&set_uuid)?,
|
|
None => MediaSet::new(),
|
|
};
|
|
|
|
Ok(MediaPool {
|
|
name: String::from(name),
|
|
state_path: state_path.to_owned(),
|
|
media_set_policy,
|
|
retention,
|
|
changer_name,
|
|
inventory,
|
|
current_media_set,
|
|
encrypt_fingerprint,
|
|
force_media_availability: false,
|
|
})
|
|
}
|
|
|
|
/// Pretend all Online(x) and Offline media is available
|
|
///
|
|
/// Only media in Vault(y) is considered unavailable.
|
|
pub fn force_media_availability(&mut self) {
|
|
self.force_media_availability = true;
|
|
}
|
|
|
|
/// Returns the Uuid of the current media set
|
|
pub fn current_media_set(&self) -> &Uuid {
|
|
self.current_media_set.uuid()
|
|
}
|
|
|
|
/// Creates a new instance using the media pool configuration
|
|
pub fn with_config(
|
|
state_path: &Path,
|
|
config: &MediaPoolConfig,
|
|
changer_name: Option<String>,
|
|
) -> Result<Self, Error> {
|
|
|
|
let allocation = config.allocation.clone().unwrap_or_else(|| String::from("continue")).parse()?;
|
|
|
|
let retention = config.retention.clone().unwrap_or_else(|| String::from("keep")).parse()?;
|
|
|
|
let encrypt_fingerprint = match config.encrypt {
|
|
Some(ref fingerprint) => Some(fingerprint.parse()?),
|
|
None => None,
|
|
};
|
|
|
|
MediaPool::new(
|
|
&config.name,
|
|
state_path,
|
|
allocation,
|
|
retention,
|
|
changer_name,
|
|
encrypt_fingerprint,
|
|
)
|
|
}
|
|
|
|
/// Returns the pool name
|
|
pub fn name(&self) -> &str {
|
|
&self.name
|
|
}
|
|
|
|
/// Returns encryption settings
|
|
pub fn encrypt_fingerprint(&self) -> Option<Fingerprint> {
|
|
self.encrypt_fingerprint.clone()
|
|
}
|
|
|
|
pub fn set_media_status_damaged(&mut self, uuid: &Uuid) -> Result<(), Error> {
|
|
self.inventory.set_media_status_damaged(uuid)
|
|
}
|
|
|
|
fn compute_media_state(&self, media_id: &MediaId) -> (MediaStatus, MediaLocation) {
|
|
|
|
let (status, location) = self.inventory.status_and_location(&media_id.label.uuid);
|
|
|
|
match status {
|
|
MediaStatus::Full | MediaStatus::Damaged | MediaStatus::Retired => {
|
|
return (status, location);
|
|
}
|
|
MediaStatus::Unknown | MediaStatus::Writable => {
|
|
/* possibly writable - fall through to check */
|
|
}
|
|
}
|
|
|
|
let set = match media_id.media_set_label {
|
|
None => return (MediaStatus::Writable, location), // not assigned to any pool
|
|
Some(ref set) => set,
|
|
};
|
|
|
|
if set.pool != self.name { // should never trigger
|
|
return (MediaStatus::Unknown, location); // belong to another pool
|
|
}
|
|
if set.uuid.as_ref() == [0u8;16] { // not assigned to any pool
|
|
return (MediaStatus::Writable, location);
|
|
}
|
|
|
|
if &set.uuid != self.current_media_set.uuid() {
|
|
return (MediaStatus::Full, location); // assume FULL
|
|
}
|
|
|
|
// media is member of current set
|
|
if self.current_media_set.is_last_media(&media_id.label.uuid) {
|
|
(MediaStatus::Writable, location) // last set member is writable
|
|
} else {
|
|
(MediaStatus::Full, location)
|
|
}
|
|
}
|
|
|
|
/// Returns the 'MediaId' with associated state
|
|
pub fn lookup_media(&self, uuid: &Uuid) -> Result<BackupMedia, Error> {
|
|
let media_id = match self.inventory.lookup_media(uuid) {
|
|
None => bail!("unable to lookup media {}", uuid),
|
|
Some(media_id) => media_id.clone(),
|
|
};
|
|
|
|
if let Some(ref set) = media_id.media_set_label {
|
|
if set.pool != self.name {
|
|
bail!("media does not belong to pool ({} != {})", set.pool, self.name);
|
|
}
|
|
}
|
|
|
|
let (status, location) = self.compute_media_state(&media_id);
|
|
|
|
Ok(BackupMedia::with_media_id(
|
|
media_id,
|
|
location,
|
|
status,
|
|
))
|
|
}
|
|
|
|
/// List all media associated with this pool
|
|
pub fn list_media(&self) -> Vec<BackupMedia> {
|
|
let media_id_list = self.inventory.list_pool_media(&self.name);
|
|
|
|
media_id_list.into_iter()
|
|
.map(|media_id| {
|
|
let (status, location) = self.compute_media_state(&media_id);
|
|
BackupMedia::with_media_id(
|
|
media_id,
|
|
location,
|
|
status,
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Set media status to FULL.
|
|
pub fn set_media_status_full(&mut self, uuid: &Uuid) -> Result<(), Error> {
|
|
let media = self.lookup_media(uuid)?; // check if media belongs to this pool
|
|
if media.status() != &MediaStatus::Full {
|
|
self.inventory.set_media_status_full(uuid)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Make sure the current media set is usable for writing
|
|
///
|
|
/// If not, starts a new media set. Also creates a new
|
|
/// set if media_set_policy implies it.
|
|
///
|
|
/// Note: We also call this in list_media to compute correct media
|
|
/// status, so this must not change persistent/saved state.
|
|
///
|
|
/// Returns the reason why we started a new media set (if we do)
|
|
pub fn start_write_session(&mut self, current_time: i64) -> Result<Option<String>, Error> {
|
|
|
|
let mut create_new_set = match self.current_set_usable() {
|
|
Err(err) => {
|
|
Some(err.to_string())
|
|
}
|
|
Ok(_) => None,
|
|
};
|
|
|
|
if create_new_set.is_none() {
|
|
match &self.media_set_policy {
|
|
MediaSetPolicy::AlwaysCreate => {
|
|
create_new_set = Some(String::from("policy is AlwaysCreate"));
|
|
}
|
|
MediaSetPolicy::CreateAt(event) => {
|
|
if let Some(set_start_time) = self.inventory.media_set_start_time(&self.current_media_set.uuid()) {
|
|
if let Ok(Some(alloc_time)) = compute_next_event(event, set_start_time as i64, false) {
|
|
if current_time >= alloc_time {
|
|
create_new_set = Some(String::from("policy CreateAt event triggered"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
MediaSetPolicy::ContinueCurrent => { /* do nothing here */ }
|
|
}
|
|
}
|
|
|
|
if create_new_set.is_some() {
|
|
let media_set = MediaSet::new();
|
|
self.current_media_set = media_set;
|
|
}
|
|
|
|
Ok(create_new_set)
|
|
}
|
|
|
|
/// List media in current media set
|
|
pub fn current_media_list(&self) -> Result<Vec<&Uuid>, Error> {
|
|
let mut list = Vec::new();
|
|
for opt_uuid in self.current_media_set.media_list().iter() {
|
|
match opt_uuid {
|
|
Some(ref uuid) => list.push(uuid),
|
|
None => bail!("current_media_list failed - media set is incomplete"),
|
|
}
|
|
}
|
|
Ok(list)
|
|
}
|
|
|
|
// tests if the media data is considered as expired at specified time
|
|
pub fn media_is_expired(&self, media: &BackupMedia, current_time: i64) -> bool {
|
|
if media.status() != &MediaStatus::Full {
|
|
return false;
|
|
}
|
|
|
|
let expire_time = self.inventory.media_expire_time(
|
|
media.id(), &self.media_set_policy, &self.retention);
|
|
|
|
current_time >= expire_time
|
|
}
|
|
|
|
// check if a location is considered on site
|
|
pub fn location_is_available(&self, location: &MediaLocation) -> bool {
|
|
match location {
|
|
MediaLocation::Online(name) => {
|
|
if self.force_media_availability {
|
|
true
|
|
} else {
|
|
if let Some(ref changer_name) = self.changer_name {
|
|
name == changer_name
|
|
} else {
|
|
// a standalone drive cannot use media currently inside a library
|
|
false
|
|
}
|
|
}
|
|
}
|
|
MediaLocation::Offline => {
|
|
if self.force_media_availability {
|
|
true
|
|
} else {
|
|
// consider available for standalone drives
|
|
self.changer_name.is_none()
|
|
}
|
|
}
|
|
MediaLocation::Vault(_) => false,
|
|
}
|
|
}
|
|
|
|
fn add_media_to_current_set(&mut self, mut media_id: MediaId, current_time: i64) -> Result<(), Error> {
|
|
|
|
let seq_nr = self.current_media_set.media_list().len() as u64;
|
|
|
|
let pool = self.name.clone();
|
|
|
|
let encrypt_fingerprint = self.encrypt_fingerprint();
|
|
|
|
let set = MediaSetLabel::with_data(
|
|
&pool,
|
|
self.current_media_set.uuid().clone(),
|
|
seq_nr,
|
|
current_time,
|
|
encrypt_fingerprint,
|
|
);
|
|
|
|
media_id.media_set_label = Some(set);
|
|
|
|
let uuid = media_id.label.uuid.clone();
|
|
|
|
let clear_media_status = true; // remove Full status
|
|
self.inventory.store(media_id, clear_media_status)?; // store persistently
|
|
|
|
self.current_media_set.add_media(uuid);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
|
|
/// Allocates a writable media to the current media set
|
|
pub fn alloc_writable_media(&mut self, current_time: i64) -> Result<Uuid, Error> {
|
|
|
|
let last_is_writable = self.current_set_usable()?;
|
|
|
|
if last_is_writable {
|
|
let last_uuid = self.current_media_set.last_media_uuid().unwrap();
|
|
let media = self.lookup_media(last_uuid)?;
|
|
return Ok(media.uuid().clone());
|
|
}
|
|
|
|
// try to find empty media in pool, add to media set
|
|
|
|
let media_list = self.list_media();
|
|
|
|
let mut empty_media = Vec::new();
|
|
let mut used_media = Vec::new();
|
|
|
|
for media in media_list.into_iter() {
|
|
if !self.location_is_available(media.location()) {
|
|
continue;
|
|
}
|
|
// already part of a media set?
|
|
if media.media_set_label().is_some() {
|
|
used_media.push(media);
|
|
} else {
|
|
// only consider writable empty media
|
|
if media.status() == &MediaStatus::Writable {
|
|
empty_media.push(media);
|
|
}
|
|
}
|
|
}
|
|
|
|
// sort empty_media, newest first -> oldest last
|
|
empty_media.sort_unstable_by(|a, b| {
|
|
let mut res = b.label().ctime.cmp(&a.label().ctime);
|
|
if res == std::cmp::Ordering::Equal {
|
|
res = b.label().label_text.cmp(&a.label().label_text);
|
|
}
|
|
res
|
|
});
|
|
|
|
if let Some(media) = empty_media.pop() {
|
|
// found empty media, add to media set an use it
|
|
let uuid = media.uuid().clone();
|
|
self.add_media_to_current_set(media.into_id(), current_time)?;
|
|
return Ok(uuid);
|
|
}
|
|
|
|
println!("no empty media in pool, try to reuse expired media");
|
|
|
|
let mut expired_media = Vec::new();
|
|
|
|
for media in used_media.into_iter() {
|
|
if let Some(set) = media.media_set_label() {
|
|
if &set.uuid == self.current_media_set.uuid() {
|
|
continue;
|
|
}
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
if self.media_is_expired(&media, current_time) {
|
|
println!("found expired media on media '{}'", media.label_text());
|
|
expired_media.push(media);
|
|
}
|
|
}
|
|
|
|
// sort expired_media, newest first -> oldest last
|
|
expired_media.sort_unstable_by(|a, b| {
|
|
let mut res = b.media_set_label().unwrap().ctime.cmp(&a.media_set_label().unwrap().ctime);
|
|
if res == std::cmp::Ordering::Equal {
|
|
res = b.label().label_text.cmp(&a.label().label_text);
|
|
}
|
|
res
|
|
});
|
|
|
|
if let Some(media) = expired_media.pop() {
|
|
println!("reuse expired media '{}'", media.label_text());
|
|
let uuid = media.uuid().clone();
|
|
self.add_media_to_current_set(media.into_id(), current_time)?;
|
|
return Ok(uuid);
|
|
}
|
|
|
|
println!("no expired media in pool, try to find unassigned/free media");
|
|
|
|
// try unassigned media
|
|
|
|
// lock artificial "__UNASSIGNED__" pool to avoid races
|
|
let _lock = MediaPool::lock(&self.state_path, "__UNASSIGNED__")?;
|
|
|
|
self.inventory.reload()?;
|
|
|
|
let mut free_media = Vec::new();
|
|
|
|
for media_id in self.inventory.list_unassigned_media() {
|
|
|
|
let (status, location) = self.compute_media_state(&media_id);
|
|
if media_id.media_set_label.is_some() { continue; } // should not happen
|
|
|
|
if !self.location_is_available(&location) {
|
|
continue;
|
|
}
|
|
|
|
// only consider writable media
|
|
if status != MediaStatus::Writable { continue; }
|
|
|
|
free_media.push(media_id);
|
|
}
|
|
|
|
// sort free_media, newest first -> oldest last
|
|
free_media.sort_unstable_by(|a, b| {
|
|
let mut res = b.label.ctime.cmp(&a.label.ctime);
|
|
if res == std::cmp::Ordering::Equal {
|
|
res = b.label.label_text.cmp(&a.label.label_text);
|
|
}
|
|
res
|
|
});
|
|
|
|
if let Some(media_id) = free_media.pop() {
|
|
println!("use free media '{}'", media_id.label.label_text);
|
|
let uuid = media_id.label.uuid.clone();
|
|
self.add_media_to_current_set(media_id, current_time)?;
|
|
return Ok(uuid);
|
|
}
|
|
|
|
bail!("alloc writable media in pool '{}' failed: no usable media found", self.name());
|
|
}
|
|
|
|
/// check if the current media set is usable for writing
|
|
///
|
|
/// This does several consistency checks, and return if
|
|
/// the last media in the current set is in writable state.
|
|
///
|
|
/// This return error when the media set must not be used any
|
|
/// longer because of consistency errors.
|
|
pub fn current_set_usable(&self) -> Result<bool, Error> {
|
|
|
|
let media_list = self.current_media_set.media_list();
|
|
|
|
let media_count = media_list.len();
|
|
if media_count == 0 {
|
|
return Ok(false);
|
|
}
|
|
|
|
let set_uuid = self.current_media_set.uuid();
|
|
let mut last_is_writable = false;
|
|
|
|
let mut last_enc: Option<Option<Fingerprint>> = None;
|
|
|
|
for (seq, opt_uuid) in media_list.iter().enumerate() {
|
|
let uuid = match opt_uuid {
|
|
None => bail!("media set is incomplete (missing media information)"),
|
|
Some(uuid) => uuid,
|
|
};
|
|
let media = self.lookup_media(uuid)?;
|
|
match media.media_set_label() {
|
|
Some(MediaSetLabel { seq_nr, uuid, ..}) if *seq_nr == seq as u64 && uuid == set_uuid => { /* OK */ },
|
|
Some(MediaSetLabel { seq_nr, uuid, ..}) if uuid == set_uuid => {
|
|
bail!("media sequence error ({} != {})", *seq_nr, seq);
|
|
},
|
|
Some(MediaSetLabel { uuid, ..}) => bail!("media owner error ({} != {}", uuid, set_uuid),
|
|
None => bail!("media owner error (no owner)"),
|
|
}
|
|
|
|
if let Some(set) = media.media_set_label() { // always true here
|
|
if set.encryption_key_fingerprint != self.encrypt_fingerprint {
|
|
bail!("pool encryption key changed");
|
|
}
|
|
match last_enc {
|
|
None => {
|
|
last_enc = Some(set.encryption_key_fingerprint.clone());
|
|
}
|
|
Some(ref last_enc) => {
|
|
if last_enc != &set.encryption_key_fingerprint {
|
|
bail!("inconsistent media encryption key");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
match media.status() {
|
|
MediaStatus::Full => { /* OK */ },
|
|
MediaStatus::Writable if (seq + 1) == media_count => {
|
|
let media_location = media.location();
|
|
if self.location_is_available(media_location) {
|
|
last_is_writable = true;
|
|
} else {
|
|
if let MediaLocation::Vault(vault) = media_location {
|
|
bail!("writable media offsite in vault '{}'", vault);
|
|
}
|
|
}
|
|
},
|
|
_ => bail!("unable to use media set - wrong media status {:?}", media.status()),
|
|
}
|
|
}
|
|
|
|
Ok(last_is_writable)
|
|
}
|
|
|
|
/// Generate a human readable name for the media set
|
|
pub fn generate_media_set_name(
|
|
&self,
|
|
media_set_uuid: &Uuid,
|
|
template: Option<String>,
|
|
) -> Result<String, Error> {
|
|
self.inventory.generate_media_set_name(media_set_uuid, template)
|
|
}
|
|
|
|
/// Lock the pool
|
|
pub fn lock(base_path: &Path, name: &str) -> Result<MediaPoolLockGuard, Error> {
|
|
let mut path = base_path.to_owned();
|
|
path.push(format!(".{}", name));
|
|
path.set_extension("lck");
|
|
|
|
let timeout = std::time::Duration::new(10, 0);
|
|
let lock = proxmox::tools::fs::open_file_locked(&path, timeout, true)?;
|
|
|
|
Ok(MediaPoolLockGuard(lock))
|
|
}
|
|
}
|
|
|
|
/// Backup media
|
|
///
|
|
/// Combines 'MediaId' with 'MediaLocation' and 'MediaStatus'
|
|
/// information.
|
|
#[derive(Debug,Serialize,Deserialize,Clone)]
|
|
pub struct BackupMedia {
|
|
/// Media ID
|
|
id: MediaId,
|
|
/// Media location
|
|
location: MediaLocation,
|
|
/// Media status
|
|
status: MediaStatus,
|
|
}
|
|
|
|
impl BackupMedia {
|
|
|
|
/// Creates a new instance
|
|
pub fn with_media_id(
|
|
id: MediaId,
|
|
location: MediaLocation,
|
|
status: MediaStatus,
|
|
) -> Self {
|
|
Self { id, location, status }
|
|
}
|
|
|
|
/// Returns the media location
|
|
pub fn location(&self) -> &MediaLocation {
|
|
&self.location
|
|
}
|
|
|
|
/// Returns the media status
|
|
pub fn status(&self) -> &MediaStatus {
|
|
&self.status
|
|
}
|
|
|
|
/// Returns the media uuid
|
|
pub fn uuid(&self) -> &Uuid {
|
|
&self.id.label.uuid
|
|
}
|
|
|
|
/// Returns the media set label
|
|
pub fn media_set_label(&self) -> Option<&MediaSetLabel> {
|
|
self.id.media_set_label.as_ref()
|
|
}
|
|
|
|
/// Returns the media creation time
|
|
pub fn ctime(&self) -> i64 {
|
|
self.id.label.ctime
|
|
}
|
|
|
|
/// Updates the media set label
|
|
pub fn set_media_set_label(&mut self, set_label: MediaSetLabel) {
|
|
self.id.media_set_label = Some(set_label);
|
|
}
|
|
|
|
/// Returns the drive label
|
|
pub fn label(&self) -> &MediaLabel {
|
|
&self.id.label
|
|
}
|
|
|
|
/// Returns the media id (drive label + media set label)
|
|
pub fn id(&self) -> &MediaId {
|
|
&self.id
|
|
}
|
|
|
|
/// Returns the media id, consumes self)
|
|
pub fn into_id(self) -> MediaId {
|
|
self.id
|
|
}
|
|
|
|
/// Returns the media label (Barcode)
|
|
pub fn label_text(&self) -> &str {
|
|
&self.id.label.label_text
|
|
}
|
|
}
|