tape: add media pool handling
This commit is contained in:
parent
9700d5374a
commit
c4d8542ec1
483
src/tape/media_pool.rs
Normal file
483
src/tape/media_pool.rs
Normal file
@ -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<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(),
|
||||
};
|
||||
|
||||
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<Self, Error> {
|
||||
|
||||
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<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.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<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 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<Uuid, Error> {
|
||||
|
||||
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<bool, Error> {
|
||||
|
||||
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<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
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user