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>
781 lines
24 KiB
Rust
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()
|
|
}
|