tape: add media state database

This commit is contained in:
Dietmar Maurer 2020-12-09 10:16:01 +01:00
parent eaff09f483
commit cafd51bf42
8 changed files with 465 additions and 10 deletions

View File

@ -1,3 +1,5 @@
use std::path::Path;
use anyhow::Error;
use serde_json::Value;
@ -14,9 +16,14 @@ use crate::{
MtxEntryKind,
},
tape::{
TAPE_STATUS_DIR,
ElementStatus,
OnlineStatusMap,
Inventory,
MediaStateDatabase,
linux_tape_changer_list,
mtx_status,
mtx_status_to_online_set,
mtx_transfer,
},
};
@ -47,8 +54,7 @@ pub fn get_status(name: String) -> Result<Vec<MtxStatusEntry>, Error> {
let status = mtx_status(&data.path)?;
/* todo: update persistent state
let state_path = Path::new(MEDIA_POOL_STATUS_DIR);
let state_path = Path::new(TAPE_STATUS_DIR);
let inventory = Inventory::load(state_path)?;
let mut map = OnlineStatusMap::new(&config)?;
@ -57,7 +63,6 @@ pub fn get_status(name: String) -> Result<Vec<MtxStatusEntry>, Error> {
let mut state_db = MediaStateDatabase::load(state_path)?;
state_db.update_online_status(&map)?;
*/
let mut list = Vec::new();

View File

@ -0,0 +1,21 @@
use ::serde::{Deserialize, Serialize};
use proxmox::api::api;
#[api()]
/// Media status
#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
/// Media Status
pub enum MediaStatus {
/// Media is ready to be written
Writable,
/// Media is full (contains data)
Full,
/// Media is marked as unknown, needs rescan
Unknown,
/// Media is marked as damaged
Damaged,
/// Media is marked as retired
Retired,
}

View File

@ -8,3 +8,6 @@ pub use drive::*;
mod media_pool;
pub use media_pool::*;
mod media_status;
pub use media_status::*;

View File

@ -38,6 +38,7 @@ async fn run() -> Result<(), Error> {
proxmox_backup::rrd::create_rrdb_dir()?;
proxmox_backup::server::jobstate::create_jobstate_dir()?;
proxmox_backup::tape::create_tape_status_dir()?;
if let Err(err) = generate_auth_key() {
bail!("unable to generate auth key - {}", err);

View File

@ -28,7 +28,7 @@ use crate::{
RetentionPolicy,
},
tape::{
MEDIA_POOL_STATUS_DIR,
TAPE_STATUS_DIR,
file_formats::{
DriveLabel,
MediaSetLabel,
@ -205,8 +205,16 @@ impl Inventory {
fn replace_file(&self) -> Result<(), Error> {
let list: Vec<&MediaId> = self.map.values().collect();
let raw = serde_json::to_string_pretty(&serde_json::to_value(list)?)?;
let options = CreateOptions::new();
let backup_user = crate::backup::backup_user()?;
let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640);
let options = CreateOptions::new()
.perm(mode)
.owner(backup_user.uid)
.group(backup_user.gid);
replace_file(&self.inventory_path, raw.as_bytes(), options)?;
Ok(())
}
@ -605,7 +613,7 @@ pub fn complete_media_uuid(
_param: &HashMap<String, String>,
) -> Vec<String> {
let inventory = match Inventory::load(Path::new(MEDIA_POOL_STATUS_DIR)) {
let inventory = match Inventory::load(Path::new(TAPE_STATUS_DIR)) {
Ok(inventory) => inventory,
Err(_) => return Vec::new(),
};
@ -619,7 +627,7 @@ pub fn complete_media_set_uuid(
_param: &HashMap<String, String>,
) -> Vec<String> {
let inventory = match Inventory::load(Path::new(MEDIA_POOL_STATUS_DIR)) {
let inventory = match Inventory::load(Path::new(TAPE_STATUS_DIR)) {
Ok(inventory) => inventory,
Err(_) => return Vec::new(),
};
@ -635,7 +643,7 @@ pub fn complete_media_changer_id(
_param: &HashMap<String, String>,
) -> Vec<String> {
let inventory = match Inventory::load(Path::new(MEDIA_POOL_STATUS_DIR)) {
let inventory = match Inventory::load(Path::new(TAPE_STATUS_DIR)) {
Ok(inventory) => inventory,
Err(_) => return Vec::new(),
};

View File

@ -0,0 +1,224 @@
use std::path::{Path, PathBuf};
use std::collections::BTreeMap;
use anyhow::Error;
use ::serde::{Deserialize, Serialize};
use serde_json::json;
use proxmox::tools::{
Uuid,
fs::{
open_file_locked,
replace_file,
file_get_json,
CreateOptions,
},
};
use crate::{
tape::{
OnlineStatusMap,
},
api2::types::{
MediaStatus,
},
};
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
/// Media location
pub enum MediaLocation {
/// Ready for use (inside tape library)
Online(String),
/// Local available, but need to be mounted (insert into tape
/// drive)
Offline,
/// Media is inside a Vault
Vault(String),
}
#[derive(Serialize,Deserialize)]
struct MediaStateEntry {
u: Uuid,
#[serde(skip_serializing_if="Option::is_none")]
l: Option<MediaLocation>,
#[serde(skip_serializing_if="Option::is_none")]
s: Option<MediaStatus>,
}
impl MediaStateEntry {
fn new(uuid: Uuid) -> Self {
MediaStateEntry { u: uuid, l: None, s: None }
}
}
/// Stores MediaLocation and MediaState persistently
pub struct MediaStateDatabase {
map: BTreeMap<Uuid, MediaStateEntry>,
database_path: PathBuf,
lockfile_path: PathBuf,
}
impl MediaStateDatabase {
pub const MEDIA_STATUS_DATABASE_FILENAME: &'static str = "media-status-db.json";
pub const MEDIA_STATUS_DATABASE_LOCKFILE: &'static str = ".media-status-db.lck";
/// Lock the database
pub fn lock(&self) -> Result<std::fs::File, Error> {
open_file_locked(&self.lockfile_path, std::time::Duration::new(10, 0), true)
}
/// 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.l.clone().unwrap_or(MediaLocation::Offline);
let status = entry.s.unwrap_or(MediaStatus::Unknown);
(status, location)
}
}
}
fn load_media_db(path: &Path) -> Result<BTreeMap<Uuid, MediaStateEntry>, Error> {
let data = file_get_json(path, Some(json!([])))?;
let list: Vec<MediaStateEntry> = serde_json::from_value(data)?;
let mut map = BTreeMap::new();
for entry in list.into_iter() {
map.insert(entry.u.clone(), entry);
}
Ok(map)
}
/// Load the database into memory
pub fn load(base_path: &Path) -> Result<MediaStateDatabase, Error> {
let mut database_path = base_path.to_owned();
database_path.push(Self::MEDIA_STATUS_DATABASE_FILENAME);
let mut lockfile_path = base_path.to_owned();
lockfile_path.push(Self::MEDIA_STATUS_DATABASE_LOCKFILE);
Ok(MediaStateDatabase {
map: Self::load_media_db(&database_path)?,
database_path,
lockfile_path,
})
}
/// Lock database, reload database, set status to Full, store database
pub fn set_media_status_full(&mut self, uuid: &Uuid) -> Result<(), Error> {
let _lock = self.lock()?;
self.map = Self::load_media_db(&self.database_path)?;
let entry = self.map.entry(uuid.clone()).or_insert(MediaStateEntry::new(uuid.clone()));
entry.s = Some(MediaStatus::Full);
self.store()
}
/// 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.database_path)?;
for (_uuid, entry) in self.map.iter_mut() {
if let Some(changer_name) = online_map.lookup_changer(&entry.u) {
entry.l = Some(MediaLocation::Online(changer_name.to_string()));
} else {
if let Some(MediaLocation::Online(ref changer_name)) = entry.l {
match online_map.online_map(changer_name) {
None => {
// no such changer device
entry.l = Some(MediaLocation::Offline);
}
Some(None) => {
// got no info - do nothing
}
Some(Some(_)) => {
// media changer changed
entry.l = Some(MediaLocation::Offline);
}
}
}
}
}
for (uuid, changer_name) in online_map.changer_map() {
if self.map.contains_key(uuid) { continue; }
let mut entry = MediaStateEntry::new(uuid.clone());
entry.l = Some(MediaLocation::Online(changer_name.to_string()));
self.map.insert(uuid.clone(), entry);
}
self.store()
}
/// Lock database, reload database, set status to Damaged, store database
pub fn set_media_status_damaged(&mut self, uuid: &Uuid) -> Result<(), Error> {
let _lock = self.lock()?;
self.map = Self::load_media_db(&self.database_path)?;
let entry = self.map.entry(uuid.clone()).or_insert(MediaStateEntry::new(uuid.clone()));
entry.s = Some(MediaStatus::Damaged);
self.store()
}
/// Lock database, reload database, set status to None, store database
pub fn clear_media_status(&mut self, uuid: &Uuid) -> Result<(), Error> {
let _lock = self.lock()?;
self.map = Self::load_media_db(&self.database_path)?;
let entry = self.map.entry(uuid.clone()).or_insert(MediaStateEntry::new(uuid.clone()));
entry.s = None ;
self.store()
}
/// Lock database, reload database, set location to vault, store database
pub fn set_media_location_vault(&mut self, uuid: &Uuid, vault: &str) -> Result<(), Error> {
let _lock = self.lock()?;
self.map = Self::load_media_db(&self.database_path)?;
let entry = self.map.entry(uuid.clone()).or_insert(MediaStateEntry::new(uuid.clone()));
entry.l = Some(MediaLocation::Vault(vault.to_string()));
self.store()
}
/// Lock database, reload database, set location to offline, store database
pub fn set_media_location_offline(&mut self, uuid: &Uuid) -> Result<(), Error> {
let _lock = self.lock()?;
self.map = Self::load_media_db(&self.database_path)?;
let entry = self.map.entry(uuid.clone()).or_insert(MediaStateEntry::new(uuid.clone()));
entry.l = Some(MediaLocation::Offline);
self.store()
}
fn store(&self) -> Result<(), Error> {
let mut list = Vec::new();
for entry in self.map.values() {
list.push(entry);
}
let raw = serde_json::to_string_pretty(&serde_json::to_value(list)?)?;
let backup_user = crate::backup::backup_user()?;
let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640);
let options = CreateOptions::new()
.perm(mode)
.owner(backup_user.uid)
.group(backup_user.gid);
replace_file(&self.database_path, raw.as_bytes(), options)?;
Ok(())
}
}

View File

@ -1,3 +1,10 @@
use anyhow::{format_err, Error};
use proxmox::tools::fs::{
create_path,
CreateOptions,
};
pub mod file_formats;
mod tape_write;
@ -18,8 +25,14 @@ pub use changer::*;
mod drive;
pub use drive::*;
/// Directory path where we stora all status information
pub const MEDIA_POOL_STATUS_DIR: &str = "/var/lib/proxmox-backup/mediapool";
mod media_state_database;
pub use media_state_database::*;
mod online_status_map;
pub use online_status_map::*;
/// Directory path where we store all tape status information
pub const TAPE_STATUS_DIR: &str = "/var/lib/proxmox-backup/tape";
/// We limit chunk archive size, so that we can faster restore a
/// specific chunk (The catalog only store file numbers, so we
@ -28,3 +41,19 @@ pub const MAX_CHUNK_ARCHIVE_SIZE: usize = 4*1024*1024*1024; // 4GB for now
/// To improve performance, we need to avoid tape drive buffer flush.
pub const COMMIT_BLOCK_SIZE: usize = 128*1024*1024*1024; // 128 GiB
/// Create tape status dir with correct permission
pub fn create_tape_status_dir() -> Result<(), Error> {
let backup_user = crate::backup::backup_user()?;
let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640);
let opts = CreateOptions::new()
.perm(mode)
.owner(backup_user.uid)
.group(backup_user.gid);
create_path(TAPE_STATUS_DIR, None, Some(opts))
.map_err(|err: Error| format_err!("unable to create tape status dir - {}", err))?;
Ok(())
}

View File

@ -0,0 +1,164 @@
use std::path::Path;
use std::collections::{HashMap, HashSet};
use anyhow::{bail, Error};
use proxmox::tools::Uuid;
use proxmox::api::section_config::SectionConfigData;
use crate::{
api2::types::{
VirtualTapeDrive,
ScsiTapeChanger,
},
tape::{
MediaChange,
Inventory,
MediaStateDatabase,
mtx_status,
mtx_status_to_online_set,
},
};
/// Helper to update media online status
///
/// A tape media is considered online if it is accessible by a changer
/// device. This class can store the list of available changes,
/// together with the accessible media ids.
pub struct OnlineStatusMap {
map: HashMap<String, Option<HashSet<Uuid>>>,
changer_map: HashMap<Uuid, String>,
}
impl OnlineStatusMap {
/// Creates a new instance with one map entry for each configured
/// changer (or 'VirtualTapeDrive', which has an internal
/// changer). The map entry is set to 'None' to indicate that we
/// do not have information about the online status.
pub fn new(config: &SectionConfigData) -> Result<Self, Error> {
let mut map = HashMap::new();
let changers: Vec<ScsiTapeChanger> = config.convert_to_typed_array("changer")?;
for changer in changers {
map.insert(changer.name.clone(), None);
}
let vtapes: Vec<VirtualTapeDrive> = config.convert_to_typed_array("virtual")?;
for vtape in vtapes {
map.insert(vtape.name.clone(), None);
}
Ok(Self { map, changer_map: HashMap::new() })
}
/// Returns the assiciated changer name for a media.
pub fn lookup_changer(&self, uuid: &Uuid) -> Option<&String> {
self.changer_map.get(uuid)
}
/// Returns the map which assiciates media uuids with changer names.
pub fn changer_map(&self) -> &HashMap<Uuid, String> {
&self.changer_map
}
/// Returns the set of online media for the specified changer.
pub fn online_map(&self, changer_name: &str) -> Option<&Option<HashSet<Uuid>>> {
self.map.get(changer_name)
}
/// Update the online set for the specified changer
pub fn update_online_status(&mut self, changer_name: &str, online_set: HashSet<Uuid>) -> Result<(), Error> {
match self.map.get(changer_name) {
None => bail!("no such changer '{}' device", changer_name),
Some(None) => { /* Ok */ },
Some(Some(_)) => {
// do not allow updates to keep self.changer_map consistent
bail!("update_online_status '{}' called twice", changer_name);
}
}
for uuid in online_set.iter() {
self.changer_map.insert(uuid.clone(), changer_name.to_string());
}
self.map.insert(changer_name.to_string(), Some(online_set));
Ok(())
}
}
/// Update online media status
///
/// Simply ask all changer devices.
pub fn update_online_status(state_path: &Path) -> Result<OnlineStatusMap, Error> {
let (config, _digest) = crate::config::drive::config()?;
let inventory = Inventory::load(state_path)?;
let changers: Vec<ScsiTapeChanger> = config.convert_to_typed_array("changer")?;
let mut map = OnlineStatusMap::new(&config)?;
for changer in changers {
let status = match mtx_status(&changer.path) {
Ok(status) => status,
Err(err) => {
eprintln!("unable to get changer '{}' status - {}", changer.name, err);
continue;
}
};
let online_set = mtx_status_to_online_set(&status, &inventory);
map.update_online_status(&changer.name, online_set)?;
}
let vtapes: Vec<VirtualTapeDrive> = config.convert_to_typed_array("virtual")?;
for vtape in vtapes {
let media_list = match vtape.list_media_changer_ids() {
Ok(media_list) => media_list,
Err(err) => {
eprintln!("unable to get changer '{}' status - {}", vtape.name, err);
continue;
}
};
let mut online_set = HashSet::new();
for changer_id in media_list {
if let Some(media_id) = inventory.find_media_by_changer_id(&changer_id) {
online_set.insert(media_id.label.uuid.clone());
}
}
map.update_online_status(&vtape.name, online_set)?;
}
let mut state_db = MediaStateDatabase::load(state_path)?;
state_db.update_online_status(&map)?;
Ok(map)
}
/// Update online media status with data from a single changer device
pub fn update_changer_online_status(
drive_config: &SectionConfigData,
inventory: &mut Inventory,
state_db: &mut MediaStateDatabase,
changer_name: &str,
changer_id_list: &Vec<String>,
) -> Result<(), Error> {
let mut online_map = OnlineStatusMap::new(drive_config)?;
let mut online_set = HashSet::new();
for changer_id in changer_id_list.iter() {
if let Some(media_id) = inventory.find_media_by_changer_id(&changer_id) {
online_set.insert(media_id.label.uuid.clone());
}
}
online_map.update_online_status(&changer_name, online_set)?;
state_db.update_online_status(&online_map)?;
Ok(())
}