Files
proxmox-backup/src/tape/inventory.rs
Thomas Lamprecht d1d74c4367 typo fixes all over the place
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>
2021-03-10 16:39:57 +01:00

781 lines
24 KiB
Rust

//! Backup media Inventory
//!
//! The Inventory persistently stores the list of known backup
//! media. A backup media is identified by its 'MediaId', which is the
//! MediaLabel/MediaSetLabel combination.
use std::collections::{HashMap, BTreeMap};
use std::path::{Path, PathBuf};
use std::os::unix::io::AsRawFd;
use anyhow::{bail, Error};
use serde::{Serialize, Deserialize};
use serde_json::json;
use proxmox::tools::{
Uuid,
fs::{
open_file_locked,
replace_file,
fchown,
file_get_json,
CreateOptions,
},
};
use crate::{
tools::systemd::time::compute_next_event,
api2::types::{
MediaSetPolicy,
RetentionPolicy,
MediaStatus,
MediaLocation,
},
tape::{
TAPE_STATUS_DIR,
MediaSet,
file_formats::{
MediaLabel,
MediaSetLabel,
},
changer::OnlineStatusMap,
},
};
/// Unique Media Identifier
///
/// This combines the label and media set label.
#[derive(Debug,Serialize,Deserialize,Clone)]
pub struct MediaId {
pub label: MediaLabel,
#[serde(skip_serializing_if="Option::is_none")]
pub media_set_label: Option<MediaSetLabel>,
}
#[derive(Serialize,Deserialize)]
struct MediaStateEntry {
id: MediaId,
#[serde(skip_serializing_if="Option::is_none")]
location: Option<MediaLocation>,
#[serde(skip_serializing_if="Option::is_none")]
status: Option<MediaStatus>,
}
/// Media Inventory
pub struct Inventory {
map: BTreeMap<Uuid, MediaStateEntry>,
inventory_path: PathBuf,
lockfile_path: PathBuf,
// helpers
media_set_start_times: HashMap<Uuid, i64>
}
impl Inventory {
pub const MEDIA_INVENTORY_FILENAME: &'static str = "inventory.json";
pub const MEDIA_INVENTORY_LOCKFILE: &'static str = ".inventory.lck";
fn new(base_path: &Path) -> Self {
let mut inventory_path = base_path.to_owned();
inventory_path.push(Self::MEDIA_INVENTORY_FILENAME);
let mut lockfile_path = base_path.to_owned();
lockfile_path.push(Self::MEDIA_INVENTORY_LOCKFILE);
Self {
map: BTreeMap::new(),
media_set_start_times: HashMap::new(),
inventory_path,
lockfile_path,
}
}
pub fn load(base_path: &Path) -> Result<Self, Error> {
let mut me = Self::new(base_path);
me.reload()?;
Ok(me)
}
/// Reload the database
pub fn reload(&mut self) -> Result<(), Error> {
self.map = Self::load_media_db(&self.inventory_path)?;
self.update_helpers();
Ok(())
}
fn update_helpers(&mut self) {
// recompute media_set_start_times
let mut set_start_times = HashMap::new();
for entry in self.map.values() {
let set = match &entry.id.media_set_label {
None => continue,
Some(set) => set,
};
if set.seq_nr == 0 {
set_start_times.insert(set.uuid.clone(), set.ctime);
}
}
self.media_set_start_times = set_start_times;
}
/// Lock the database
pub fn lock(&self) -> Result<std::fs::File, Error> {
let file = open_file_locked(&self.lockfile_path, std::time::Duration::new(10, 0), true)?;
if cfg!(test) {
// We cannot use chown inside test environment (no permissions)
return Ok(file);
}
let backup_user = crate::backup::backup_user()?;
fchown(file.as_raw_fd(), Some(backup_user.uid), Some(backup_user.gid))?;
Ok(file)
}
fn load_media_db(path: &Path) -> Result<BTreeMap<Uuid, MediaStateEntry>, Error> {
let data = file_get_json(path, Some(json!([])))?;
let media_list: Vec<MediaStateEntry> = serde_json::from_value(data)?;
let mut map = BTreeMap::new();
for entry in media_list.into_iter() {
map.insert(entry.id.label.uuid.clone(), entry);
}
Ok(map)
}
fn replace_file(&self) -> Result<(), Error> {
let list: Vec<&MediaStateEntry> = self.map.values().collect();
let raw = serde_json::to_string_pretty(&serde_json::to_value(list)?)?;
let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640);
let options = if cfg!(test) {
// We cannot use chown inside test environment (no permissions)
CreateOptions::new().perm(mode)
} else {
let backup_user = crate::backup::backup_user()?;
CreateOptions::new()
.perm(mode)
.owner(backup_user.uid)
.group(backup_user.gid)
};
replace_file(&self.inventory_path, raw.as_bytes(), options)?;
Ok(())
}
/// Stores a single MediaID persistently
pub fn store(
&mut self,
mut media_id: MediaId,
clear_media_status: bool,
) -> Result<(), Error> {
let _lock = self.lock()?;
self.map = Self::load_media_db(&self.inventory_path)?;
let uuid = media_id.label.uuid.clone();
if let Some(previous) = self.map.remove(&media_id.label.uuid) {
// do not overwrite unsaved pool assignments
if media_id.media_set_label.is_none() {
if let Some(ref set) = previous.id.media_set_label {
if set.uuid.as_ref() == [0u8;16] {
media_id.media_set_label = Some(set.clone());
}
}
}
let entry = MediaStateEntry {
id: media_id,
location: previous.location,
status: if clear_media_status {
None
} else {
previous.status
},
};
self.map.insert(uuid, entry);
} else {
let entry = MediaStateEntry { id: media_id, location: None, status: None };
self.map.insert(uuid, entry);
}
self.update_helpers();
self.replace_file()?;
Ok(())
}
/// Remove a single media persistently
pub fn remove_media(&mut self, uuid: &Uuid) -> Result<(), Error> {
let _lock = self.lock()?;
self.map = Self::load_media_db(&self.inventory_path)?;
self.map.remove(uuid);
self.update_helpers();
self.replace_file()?;
Ok(())
}
/// Lookup media
pub fn lookup_media(&self, uuid: &Uuid) -> Option<&MediaId> {
self.map.get(uuid).map(|entry| &entry.id)
}
/// List all media Uuids
pub fn media_list(&self) -> Vec<&Uuid> {
self.map.keys().collect()
}
/// find media by label_text
pub fn find_media_by_label_text(&self, label_text: &str) -> Option<&MediaId> {
self.map.values().find_map(|entry| {
if entry.id.label.label_text == label_text {
Some(&entry.id)
} else {
None
}
})
}
/// Lookup media pool
///
/// Returns (pool, is_empty)
pub fn lookup_media_pool(&self, uuid: &Uuid) -> Option<(&str, bool)> {
match self.map.get(uuid) {
None => None,
Some(entry) => {
match entry.id.media_set_label {
None => None, // not assigned to any pool
Some(ref set) => {
let is_empty = set.uuid.as_ref() == [0u8;16];
Some((&set.pool, is_empty))
}
}
}
}
}
/// List all media assigned to the pool
pub fn list_pool_media(&self, pool: &str) -> Vec<MediaId> {
let mut list = Vec::new();
for entry in self.map.values() {
match entry.id.media_set_label {
None => continue, // not assigned to any pool
Some(ref set) => {
if set.pool != pool {
continue; // belong to another pool
}
if set.uuid.as_ref() == [0u8;16] {
list.push(MediaId {
label: entry.id.label.clone(),
media_set_label: None,
})
} else {
list.push(entry.id.clone());
}
}
}
}
list
}
/// List all used media
pub fn list_used_media(&self) -> Vec<MediaId> {
let mut list = Vec::new();
for entry in self.map.values() {
match entry.id.media_set_label {
None => continue, // not assigned to any pool
Some(ref set) => {
if set.uuid.as_ref() != [0u8;16] {
list.push(entry.id.clone());
}
}
}
}
list
}
/// List media not assigned to any pool
pub fn list_unassigned_media(&self) -> Vec<MediaId> {
self.map.values().filter_map(|entry|
if entry.id.media_set_label.is_none() {
Some(entry.id.clone())
} else {
None
}
).collect()
}
pub fn media_set_start_time(&self, media_set_uuid: &Uuid) -> Option<i64> {
self.media_set_start_times.get(media_set_uuid).copied()
}
/// Lookup media set pool
pub fn lookup_media_set_pool(&self, media_set_uuid: &Uuid) -> Result<String, Error> {
let mut last_pool = None;
for entry in self.map.values() {
match entry.id.media_set_label {
None => continue,
Some(MediaSetLabel { ref uuid, .. }) => {
if uuid != media_set_uuid {
continue;
}
if let Some((pool, _)) = self.lookup_media_pool(&entry.id.label.uuid) {
if let Some(last_pool) = last_pool {
if last_pool != pool {
bail!("detected media set with inconsistent pool assignment - internal error");
}
} else {
last_pool = Some(pool);
}
}
}
}
}
match last_pool {
Some(pool) => Ok(pool.to_string()),
None => bail!("media set {} is incomplete - unable to lookup pool", media_set_uuid),
}
}
/// Compute a single media sets
pub fn compute_media_set_members(&self, media_set_uuid: &Uuid) -> Result<MediaSet, Error> {
let mut set = MediaSet::with_data(media_set_uuid.clone(), Vec::new());
for entry in self.map.values() {
match entry.id.media_set_label {
None => continue,
Some(MediaSetLabel { seq_nr, ref uuid, .. }) => {
if uuid != media_set_uuid {
continue;
}
set.insert_media(entry.id.label.uuid.clone(), seq_nr)?;
}
}
}
Ok(set)
}
/// Compute all media sets
pub fn compute_media_set_list(&self) -> Result<HashMap<Uuid, MediaSet>, Error> {
let mut set_map: HashMap<Uuid, MediaSet> = HashMap::new();
for entry in self.map.values() {
match entry.id.media_set_label {
None => continue,
Some(MediaSetLabel { seq_nr, ref uuid, .. }) => {
let set = set_map.entry(uuid.clone()).or_insert_with(|| {
MediaSet::with_data(uuid.clone(), Vec::new())
});
set.insert_media(entry.id.label.uuid.clone(), seq_nr)?;
}
}
}
Ok(set_map)
}
/// Returns the latest media set for a pool
pub fn latest_media_set(&self, pool: &str) -> Option<Uuid> {
let mut last_set: Option<(Uuid, i64)> = None;
let set_list = self.map.values()
.filter_map(|entry| entry.id.media_set_label.as_ref())
.filter(|set| set.pool == pool && set.uuid.as_ref() != [0u8;16]);
for set in set_list {
match last_set {
None => {
last_set = Some((set.uuid.clone(), set.ctime));
}
Some((_, last_ctime)) => {
if set.ctime > last_ctime {
last_set = Some((set.uuid.clone(), set.ctime));
}
}
}
}
let (uuid, ctime) = match last_set {
None => return None,
Some((uuid, ctime)) => (uuid, ctime),
};
// consistency check - must be the only set with that ctime
let set_list = self.map.values()
.filter_map(|entry| entry.id.media_set_label.as_ref())
.filter(|set| set.pool == pool && set.uuid.as_ref() != [0u8;16]);
for set in set_list {
if set.uuid != uuid && set.ctime >= ctime { // should not happen
eprintln!("latest_media_set: found set with equal ctime ({}, {})", set.uuid, uuid);
return None;
}
}
Some(uuid)
}
// Test if there is a media set (in the same pool) newer than this one.
// Return the ctime of the nearest media set
fn media_set_next_start_time(&self, media_set_uuid: &Uuid) -> Option<i64> {
let (pool, ctime) = match self.map.values()
.filter_map(|entry| entry.id.media_set_label.as_ref())
.find_map(|set| {
if &set.uuid == media_set_uuid {
Some((set.pool.clone(), set.ctime))
} else {
None
}
}) {
Some((pool, ctime)) => (pool, ctime),
None => return None,
};
let set_list = self.map.values()
.filter_map(|entry| entry.id.media_set_label.as_ref())
.filter(|set| (&set.uuid != media_set_uuid) && (set.pool == pool));
let mut next_ctime = None;
for set in set_list {
if set.ctime > ctime {
match next_ctime {
None => {
next_ctime = Some(set.ctime);
}
Some(last_next_ctime) => {
if set.ctime < last_next_ctime {
next_ctime = Some(set.ctime);
}
}
}
}
}
next_ctime
}
pub fn media_expire_time(
&self,
media: &MediaId,
media_set_policy: &MediaSetPolicy,
retention_policy: &RetentionPolicy,
) -> i64 {
if let RetentionPolicy::KeepForever = retention_policy {
return i64::MAX;
}
let set = match media.media_set_label {
None => return i64::MAX,
Some(ref set) => set,
};
let set_start_time = match self.media_set_start_time(&set.uuid) {
None => {
// missing information, use ctime from this
// set (always greater than ctime from seq_nr 0)
set.ctime
}
Some(time) => time,
};
let max_use_time = match self.media_set_next_start_time(&set.uuid) {
Some(next_start_time) => {
match media_set_policy {
MediaSetPolicy::AlwaysCreate => set_start_time,
_ => next_start_time,
}
}
None => {
match media_set_policy {
MediaSetPolicy::ContinueCurrent => {
return i64::MAX;
}
MediaSetPolicy::AlwaysCreate => {
set_start_time
}
MediaSetPolicy::CreateAt(ref event) => {
match compute_next_event(event, set_start_time, false) {
Ok(Some(next)) => next,
Ok(None) | Err(_) => return i64::MAX,
}
}
}
}
};
match retention_policy {
RetentionPolicy::KeepForever => i64::MAX,
RetentionPolicy::OverwriteAlways => max_use_time,
RetentionPolicy::ProtectFor(time_span) => {
let seconds = f64::from(time_span.clone()) as i64;
max_use_time + seconds
}
}
}
/// Generate a human readable name for the media set
///
/// The template can include strftime time format specifications.
pub fn generate_media_set_name(
&self,
media_set_uuid: &Uuid,
template: Option<String>,
) -> Result<String, Error> {
if let Some(ctime) = self.media_set_start_time(media_set_uuid) {
let mut template = template.unwrap_or_else(|| String::from("%c"));
template = template.replace("%id%", &media_set_uuid.to_string());
proxmox::tools::time::strftime_local(&template, ctime)
} else {
// We don't know the set start time, so we cannot use the template
Ok(media_set_uuid.to_string())
}
}
// Helpers to simplify testing
/// Generate and insert a new free tape (test helper)
pub fn generate_free_tape(&mut self, label_text: &str, ctime: i64) -> Uuid {
let label = MediaLabel {
label_text: label_text.to_string(),
uuid: Uuid::generate(),
ctime,
};
let uuid = label.uuid.clone();
self.store(MediaId { label, media_set_label: None }, false).unwrap();
uuid
}
/// Generate and insert a new tape assigned to a specific pool
/// (test helper)
pub fn generate_assigned_tape(
&mut self,
label_text: &str,
pool: &str,
ctime: i64,
) -> Uuid {
let label = MediaLabel {
label_text: label_text.to_string(),
uuid: Uuid::generate(),
ctime,
};
let uuid = label.uuid.clone();
let set = MediaSetLabel::with_data(pool, [0u8; 16].into(), 0, ctime, None);
self.store(MediaId { label, media_set_label: Some(set) }, false).unwrap();
uuid
}
/// Generate and insert a used tape (test helper)
pub fn generate_used_tape(
&mut self,
label_text: &str,
set: MediaSetLabel,
ctime: i64,
) -> Uuid {
let label = MediaLabel {
label_text: label_text.to_string(),
uuid: Uuid::generate(),
ctime,
};
let uuid = label.uuid.clone();
self.store(MediaId { label, media_set_label: Some(set) }, false).unwrap();
uuid
}
}
// Status/location handling
impl Inventory {
/// Returns status and location with reasonable defaults.
///
/// Default status is 'MediaStatus::Unknown'.
/// Default location is 'MediaLocation::Offline'.
pub fn status_and_location(&self, uuid: &Uuid) -> (MediaStatus, MediaLocation) {
match self.map.get(uuid) {
None => {
// no info stored - assume media is writable/offline
(MediaStatus::Unknown, MediaLocation::Offline)
}
Some(entry) => {
let location = entry.location.clone().unwrap_or(MediaLocation::Offline);
let status = entry.status.unwrap_or(MediaStatus::Unknown);
(status, location)
}
}
}
// Lock database, reload database, set status, store database
fn set_media_status(&mut self, uuid: &Uuid, status: Option<MediaStatus>) -> Result<(), Error> {
let _lock = self.lock()?;
self.map = Self::load_media_db(&self.inventory_path)?;
if let Some(entry) = self.map.get_mut(uuid) {
entry.status = status;
self.update_helpers();
self.replace_file()?;
Ok(())
} else {
bail!("no such media '{}'", uuid);
}
}
/// Lock database, reload database, set status to Full, store database
pub fn set_media_status_full(&mut self, uuid: &Uuid) -> Result<(), Error> {
self.set_media_status(uuid, Some(MediaStatus::Full))
}
/// Lock database, reload database, set status to Damaged, store database
pub fn set_media_status_damaged(&mut self, uuid: &Uuid) -> Result<(), Error> {
self.set_media_status(uuid, Some(MediaStatus::Damaged))
}
/// Lock database, reload database, set status to Retired, store database
pub fn set_media_status_retired(&mut self, uuid: &Uuid) -> Result<(), Error> {
self.set_media_status(uuid, Some(MediaStatus::Retired))
}
/// Lock database, reload database, set status to None, store database
pub fn clear_media_status(&mut self, uuid: &Uuid) -> Result<(), Error> {
self.set_media_status(uuid, None)
}
// Lock database, reload database, set location, store database
fn set_media_location(&mut self, uuid: &Uuid, location: Option<MediaLocation>) -> Result<(), Error> {
let _lock = self.lock()?;
self.map = Self::load_media_db(&self.inventory_path)?;
if let Some(entry) = self.map.get_mut(uuid) {
entry.location = location;
self.update_helpers();
self.replace_file()?;
Ok(())
} else {
bail!("no such media '{}'", uuid);
}
}
/// Lock database, reload database, set location to vault, store database
pub fn set_media_location_vault(&mut self, uuid: &Uuid, vault: &str) -> Result<(), Error> {
self.set_media_location(uuid, Some(MediaLocation::Vault(vault.to_string())))
}
/// Lock database, reload database, set location to offline, store database
pub fn set_media_location_offline(&mut self, uuid: &Uuid) -> Result<(), Error> {
self.set_media_location(uuid, Some(MediaLocation::Offline))
}
/// Update online status
pub fn update_online_status(&mut self, online_map: &OnlineStatusMap) -> Result<(), Error> {
let _lock = self.lock()?;
self.map = Self::load_media_db(&self.inventory_path)?;
for (uuid, entry) in self.map.iter_mut() {
if let Some(changer_name) = online_map.lookup_changer(uuid) {
entry.location = Some(MediaLocation::Online(changer_name.to_string()));
} else if let Some(MediaLocation::Online(ref changer_name)) = entry.location {
match online_map.online_map(changer_name) {
None => {
// no such changer device
entry.location = Some(MediaLocation::Offline);
}
Some(None) => {
// got no info - do nothing
}
Some(Some(_)) => {
// media changer changed
entry.location = Some(MediaLocation::Offline);
}
}
}
}
self.update_helpers();
self.replace_file()?;
Ok(())
}
}
// shell completion helper
/// List of known media uuids
pub fn complete_media_uuid(
_arg: &str,
_param: &HashMap<String, String>,
) -> Vec<String> {
let inventory = match Inventory::load(Path::new(TAPE_STATUS_DIR)) {
Ok(inventory) => inventory,
Err(_) => return Vec::new(),
};
inventory.map.keys().map(|uuid| uuid.to_string()).collect()
}
/// List of known media sets
pub fn complete_media_set_uuid(
_arg: &str,
_param: &HashMap<String, String>,
) -> Vec<String> {
let inventory = match Inventory::load(Path::new(TAPE_STATUS_DIR)) {
Ok(inventory) => inventory,
Err(_) => return Vec::new(),
};
inventory.map.values()
.filter_map(|entry| entry.id.media_set_label.as_ref())
.map(|set| set.uuid.to_string()).collect()
}
/// List of known media labels (barcodes)
pub fn complete_media_label_text(
_arg: &str,
_param: &HashMap<String, String>,
) -> Vec<String> {
let inventory = match Inventory::load(Path::new(TAPE_STATUS_DIR)) {
Ok(inventory) => inventory,
Err(_) => return Vec::new(),
};
inventory.map.values().map(|entry| entry.id.label.label_text.clone()).collect()
}