]> git.proxmox.com Git - proxmox-backup.git/commitdiff
tape: add media state database
authorDietmar Maurer <dietmar@proxmox.com>
Wed, 9 Dec 2020 09:16:01 +0000 (10:16 +0100)
committerDietmar Maurer <dietmar@proxmox.com>
Wed, 9 Dec 2020 10:21:56 +0000 (11:21 +0100)
src/api2/tape/changer.rs
src/api2/types/tape/media_status.rs [new file with mode: 0644]
src/api2/types/tape/mod.rs
src/bin/proxmox-backup-api.rs
src/tape/inventory.rs
src/tape/media_state_database.rs [new file with mode: 0644]
src/tape/mod.rs
src/tape/online_status_map.rs [new file with mode: 0644]

index 77829d3afd9cdcdc03df9a0b274f6a04b4ccc1d3..1fd79af3958556931e5c959a02b089b7d0268fa1 100644 (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();
 
diff --git a/src/api2/types/tape/media_status.rs b/src/api2/types/tape/media_status.rs
new file mode 100644 (file)
index 0000000..5a3bff9
--- /dev/null
@@ -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,
+}
index a6b4689bcfc860e1782a6c8ef35e98ffff7421d3..6c3b8770411895a231bf0117619b1ff29781eeaa 100644 (file)
@@ -8,3 +8,6 @@ pub use drive::*;
 
 mod media_pool;
 pub use media_pool::*;
+
+mod media_status;
+pub use media_status::*;
index 70d4cb5d075088b35fb099babef7e0e53491318d..cf61b85cf2b53e9dc85234e465636411439cc8d3 100644 (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);
index 63f85ba21b53b50bbf9cbd9743549779a661c381..55e5bc3de69ff0b41ad1ae653d5040beb09dcaa7 100644 (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(),
     };
diff --git a/src/tape/media_state_database.rs b/src/tape/media_state_database.rs
new file mode 100644 (file)
index 0000000..270e262
--- /dev/null
@@ -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(())
+    }
+}
index 34798a23caed364cce3c70ce97c6be830bdbd85a..ac6802af91ad85432e78a0bbf39ed311d695a21b 100644 (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(())
+}
diff --git a/src/tape/online_status_map.rs b/src/tape/online_status_map.rs
new file mode 100644 (file)
index 0000000..f809c82
--- /dev/null
@@ -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(())
+}