diff --git a/src/tape/media_pool.rs b/src/tape/media_pool.rs new file mode 100644 index 00000000..4064d32e --- /dev/null +++ b/src/tape/media_pool.rs @@ -0,0 +1,483 @@ +//! Media Pool +//! +//! A set of backup medias. +//! +//! This struct manages backup media state during backup. The main +//! purpose ist to allocate media sets and assing new tapes to it. +//! +//! + +use std::path::Path; +use anyhow::{bail, Error}; +use ::serde::{Deserialize, Serialize}; + +use proxmox::tools::Uuid; + +use crate::{ + api2::types::{ + MediaStatus, + MediaSetPolicy, + RetentionPolicy, + MediaPoolConfig, + }, + tools::systemd::time::compute_next_event, + tape::{ + MediaId, + MediaSet, + MediaLocation, + Inventory, + MediaStateDatabase, + file_formats::{ + DriveLabel, + MediaSetLabel, + }, + } +}; + +pub struct MediaPoolLockGuard(std::fs::File); + +/// Media Pool +pub struct MediaPool { + + name: String, + + media_set_policy: MediaSetPolicy, + retention: RetentionPolicy, + + inventory: Inventory, + state_db: MediaStateDatabase, + + current_media_set: MediaSet, +} + +impl MediaPool { + + /// Creates a new instance + pub fn new( + name: &str, + state_path: &Path, + media_set_policy: MediaSetPolicy, + retention: RetentionPolicy, + ) -> Result { + + 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(), + }; + + let state_db = MediaStateDatabase::load(state_path)?; + + Ok(MediaPool { + name: String::from(name), + media_set_policy, + retention, + inventory, + state_db, + current_media_set, + }) + } + + /// Creates a new instance using the media pool configuration + pub fn with_config( + name: &str, + state_path: &Path, + config: &MediaPoolConfig, + ) -> Result { + + let allocation = config.allocation.clone().unwrap_or(String::from("continue")).parse()?; + + let retention = config.retention.clone().unwrap_or(String::from("keep")).parse()?; + + MediaPool::new(name, state_path, allocation, retention) + } + + /// Returns the pool name + pub fn name(&self) -> &str { + &self.name + } + + fn compute_media_state(&self, media_id: &MediaId) -> (MediaStatus, MediaLocation) { + + let (status, location) = self.state_db.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 { + 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 { + 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.state_db.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. + pub fn start_write_session(&mut self, current_time: i64) -> Result<(), Error> { + + let mut create_new_set = match self.current_set_usable() { + Err(err) => { + eprintln!("unable to use current media set - {}", err); + true + } + Ok(usable) => !usable, + }; + + if !create_new_set { + + match &self.media_set_policy { + MediaSetPolicy::AlwaysCreate => { + create_new_set = true; + } + 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 = true; + } + } + } + } + MediaSetPolicy::ContinueCurrent => { /* do nothing here */ } + } + } + + if create_new_set { + let media_set = MediaSet::new(); + eprintln!("starting new media set {}", media_set.uuid()); + self.current_media_set = media_set; + } + + Ok(()) + } + + /// List media in current media set + pub fn current_media_list(&self) -> Result, 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 sepcified 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 + } + + /// Allocates a writable media to the current media set + pub fn alloc_writable_media(&mut self, current_time: i64) -> Result { + + let last_is_writable = self.current_set_usable()?; + + let pool = self.name.clone(); + + 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 mut media_list = self.list_media(); + + let mut empty_media = Vec::new(); + for media in media_list.iter_mut() { + // already part of a media set? + if media.media_set_label().is_some() { continue; } + + // check if media is on site + match media.location() { + MediaLocation::Online(_) | MediaLocation::Offline => { /* OK */ }, + MediaLocation::Vault(_) => continue, + } + + // only consider writable media + if media.status() != &MediaStatus::Writable { continue; } + + empty_media.push(media); + } + + // sort empty_media, oldest media first + empty_media.sort_unstable_by_key(|media| media.label().ctime); + + if let Some(media) = empty_media.first_mut() { + // found empty media, add to media set an use it + let seq_nr = self.current_media_set.media_list().len() as u64; + + let set = MediaSetLabel::with_data(&pool, self.current_media_set.uuid().clone(), seq_nr, current_time); + + media.set_media_set_label(set); + + self.inventory.store(media.id().clone())?; // store persistently + + self.current_media_set.add_media(media.uuid().clone()); + + return Ok(media.uuid().clone()); + } + + println!("no empty media in pool, try to reuse expired media"); + + let mut expired_media = Vec::new(); + + for media in media_list.into_iter() { + if let Some(set) = media.media_set_label() { + if &set.uuid == self.current_media_set.uuid() { + continue; + } + } + if self.media_is_expired(&media, current_time) { + println!("found expired media on media '{}'", media.changer_id()); + expired_media.push(media); + } + } + + // sort, oldest media first + expired_media.sort_unstable_by_key(|media| { + match media.media_set_label() { + None => 0, // should not happen here + Some(set) => set.ctime, + } + }); + + match expired_media.first_mut() { + None => { + bail!("alloc writable media in pool '{}' failed: no usable media found", self.name()); + } + Some(media) => { + println!("reuse expired media '{}'", media.changer_id()); + + let seq_nr = self.current_media_set.media_list().len() as u64; + let set = MediaSetLabel::with_data(&pool, self.current_media_set.uuid().clone(), seq_nr, current_time); + + media.set_media_set_label(set); + + self.inventory.store(media.id().clone())?; // store persistently + self.state_db.clear_media_status(media.uuid())?; // remove Full status + + self.current_media_set.add_media(media.uuid().clone()); + + return Ok(media.uuid().clone()); + } + } + } + + /// 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 { + + let media_count = self.current_media_set.media_list().len(); + if media_count == 0 { + return Ok(false); + } + + let set_uuid = self.current_media_set.uuid(); + let mut last_is_writable = false; + + for (seq, opt_uuid) in self.current_media_set.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)"), + } + match media.status() { + MediaStatus::Full => { /* OK */ }, + MediaStatus::Writable if (seq + 1) == media_count => { + last_is_writable = true; + match media.location() { + MediaLocation::Online(_) | MediaLocation::Offline => { /* OK */ }, + MediaLocation::Vault(vault) => { + 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, + ) -> Result { + self.inventory.generate_media_set_name(media_set_uuid, template) + } + + /// Lock the pool + pub fn lock(base_path: &Path, name: &str) -> Result { + 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 { + &self.id.media_set_label + } + + /// 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) -> &DriveLabel { + &self.id.label + } + + /// Returns the media id (drive label + media set label) + pub fn id(&self) -> &MediaId { + &self.id + } + + /// Returns the media label (Barcode) + pub fn changer_id(&self) -> &str { + &self.id.label.changer_id + } +} diff --git a/src/tape/mod.rs b/src/tape/mod.rs index ac6802af..9bb6bb6e 100644 --- a/src/tape/mod.rs +++ b/src/tape/mod.rs @@ -31,6 +31,9 @@ pub use media_state_database::*; mod online_status_map; pub use online_status_map::*; +mod media_pool; +pub use media_pool::*; + /// Directory path where we store all tape status information pub const TAPE_STATUS_DIR: &str = "/var/lib/proxmox-backup/tape";