diff --git a/src/api2/types/mod.rs b/src/api2/types/mod.rs index 9d753f86..59b2433f 100644 --- a/src/api2/types/mod.rs +++ b/src/api2/types/mod.rs @@ -23,7 +23,6 @@ pub use userid::{PROXMOX_TOKEN_ID_SCHEMA, PROXMOX_TOKEN_NAME_SCHEMA, PROXMOX_GRO mod tape; pub use tape::*; - // File names: may not contain slashes, may not start with "." pub const FILENAME_FORMAT: ApiStringFormat = ApiStringFormat::VerifyFn(|name| { if name.starts_with('.') { diff --git a/src/api2/types/tape.rs b/src/api2/types/tape/drive.rs similarity index 96% rename from src/api2/types/tape.rs rename to src/api2/types/tape/drive.rs index 379e9102..34d81b74 100644 --- a/src/api2/types/tape.rs +++ b/src/api2/types/tape/drive.rs @@ -1,5 +1,5 @@ -//! Types for tape backup API -//! +//! Types for tape drive API + use serde::{Deserialize, Serialize}; use proxmox::api::{ @@ -7,7 +7,7 @@ use proxmox::api::{ schema::{Schema, StringSchema}, }; -use super::PROXMOX_SAFE_ID_FORMAT; +use crate::api2::types::PROXMOX_SAFE_ID_FORMAT; pub const DRIVE_ID_SCHEMA: Schema = StringSchema::new("Drive Identifier.") .format(&PROXMOX_SAFE_ID_FORMAT) diff --git a/src/api2/types/tape/media_pool.rs b/src/api2/types/tape/media_pool.rs new file mode 100644 index 00000000..da3508d2 --- /dev/null +++ b/src/api2/types/tape/media_pool.rs @@ -0,0 +1,154 @@ +//! Types for tape media pool API +//! +//! Note: Both MediaSetPolicy and RetentionPolicy are complex enums, +//! so we cannot use them directly for the API. Instead, we represent +//! them as String. + +use anyhow::Error; +use std::str::FromStr; +use serde::{Deserialize, Serialize}; + +use proxmox::api::{ + api, + schema::{Schema, StringSchema, ApiStringFormat}, +}; + +use crate::{ + tools::systemd::time::{ + CalendarEvent, + TimeSpan, + parse_time_span, + parse_calendar_event, + }, + api2::types::{ + DRIVE_ID_SCHEMA, + PROXMOX_SAFE_ID_FORMAT, + SINGLE_LINE_COMMENT_FORMAT, + }, +}; + +pub const MEDIA_POOL_NAME_SCHEMA: Schema = StringSchema::new("Media pool name.") + .format(&PROXMOX_SAFE_ID_FORMAT) + .min_length(3) + .max_length(32) + .schema(); + +pub const MEDIA_SET_NAMING_TEMPLATE_SCHEMA: Schema = StringSchema::new( + "Media set naming template.") + .format(&SINGLE_LINE_COMMENT_FORMAT) + .min_length(2) + .max_length(64) + .schema(); + +pub const MEDIA_SET_ALLOCATION_POLICY_FORMAT: ApiStringFormat = + ApiStringFormat::VerifyFn(|s| { MediaSetPolicy::from_str(s)?; Ok(()) }); + +pub const MEDIA_SET_ALLOCATION_POLICY_SCHEMA: Schema = StringSchema::new( + "Media set allocation policy.") + .format(&MEDIA_SET_ALLOCATION_POLICY_FORMAT) + .schema(); + +/// Media set allocation policy +pub enum MediaSetPolicy { + /// Try to use the current media set + ContinueCurrent, + /// Each backup job creates a new media set + AlwaysCreate, + /// Create a new set when the specified CalendarEvent triggers + CreateAt(CalendarEvent), +} + +impl std::str::FromStr for MediaSetPolicy { + type Err = Error; + + fn from_str(s: &str) -> Result { + if s == "continue" { + return Ok(MediaSetPolicy::ContinueCurrent); + } + if s == "always" { + return Ok(MediaSetPolicy::AlwaysCreate); + } + + let event = parse_calendar_event(s)?; + + Ok(MediaSetPolicy::CreateAt(event)) + } +} + +pub const MEDIA_RETENTION_POLICY_FORMAT: ApiStringFormat = + ApiStringFormat::VerifyFn(|s| { RetentionPolicy::from_str(s)?; Ok(()) }); + +pub const MEDIA_RETENTION_POLICY_SCHEMA: Schema = StringSchema::new( + "Media retention policy.") + .format(&MEDIA_RETENTION_POLICY_FORMAT) + .schema(); + +/// Media retention Policy +pub enum RetentionPolicy { + /// Always overwrite media + OverwriteAlways, + /// Protect data for the timespan specified + ProtectFor(TimeSpan), + /// Never overwrite data + KeepForever, +} + +impl std::str::FromStr for RetentionPolicy { + type Err = Error; + + fn from_str(s: &str) -> Result { + if s == "overwrite" { + return Ok(RetentionPolicy::OverwriteAlways); + } + if s == "keep" { + return Ok(RetentionPolicy::KeepForever); + } + + let time_span = parse_time_span(s)?; + + Ok(RetentionPolicy::ProtectFor(time_span)) + } +} + +#[api( + properties: { + name: { + schema: MEDIA_POOL_NAME_SCHEMA, + }, + drive: { + schema: DRIVE_ID_SCHEMA, + }, + allocation: { + schema: MEDIA_SET_ALLOCATION_POLICY_SCHEMA, + optional: true, + }, + retention: { + schema: MEDIA_RETENTION_POLICY_SCHEMA, + optional: true, + }, + template: { + schema: MEDIA_SET_NAMING_TEMPLATE_SCHEMA, + optional: true, + }, + } +)] +#[derive(Serialize,Deserialize)] +/// Media pool configuration +pub struct MediaPoolConfig { + /// The pool name + pub name: String, + /// The associated drive + pub drive: String, + /// Media Set allocation policy + #[serde(skip_serializing_if="Option::is_none")] + pub allocation: Option, + /// Media retention policy + #[serde(skip_serializing_if="Option::is_none")] + pub retention: Option, + /// Media set naming template (default "%id%") + /// + /// The template is UTF8 text, and can include strftime time + /// format specifications. + #[serde(skip_serializing_if="Option::is_none")] + pub template: Option, +} diff --git a/src/api2/types/tape/mod.rs b/src/api2/types/tape/mod.rs new file mode 100644 index 00000000..1c59ab0f --- /dev/null +++ b/src/api2/types/tape/mod.rs @@ -0,0 +1,7 @@ +//! Types for tape backup API + +mod drive; +pub use drive::*; + +mod media_pool; +pub use media_pool::*; diff --git a/src/config.rs b/src/config.rs index 91c85e53..aa767811 100644 --- a/src/config.rs +++ b/src/config.rs @@ -25,6 +25,7 @@ pub mod token_shadow; pub mod user; pub mod verify; pub mod drive; +pub mod media_pool; /// Check configuration directory permissions /// diff --git a/src/config/media_pool.rs b/src/config/media_pool.rs new file mode 100644 index 00000000..e7d66f42 --- /dev/null +++ b/src/config/media_pool.rs @@ -0,0 +1,88 @@ +use std::collections::HashMap; + +use anyhow::Error; +use lazy_static::lazy_static; + +use proxmox::{ + api::{ + schema::*, + section_config::{ + SectionConfig, + SectionConfigData, + SectionConfigPlugin, + } + }, + tools::fs::{ + open_file_locked, + replace_file, + CreateOptions, + }, +}; + +use crate::{ + api2::types::{ + MEDIA_POOL_NAME_SCHEMA, + MediaPoolConfig, + }, +}; + +lazy_static! { + static ref CONFIG: SectionConfig = init(); +} + +fn init() -> SectionConfig { + let mut config = SectionConfig::new(&MEDIA_POOL_NAME_SCHEMA); + + let obj_schema = match MediaPoolConfig::API_SCHEMA { + Schema::Object(ref obj_schema) => obj_schema, + _ => unreachable!(), + }; + let plugin = SectionConfigPlugin::new("pool".to_string(), Some("name".to_string()), obj_schema); + config.register_plugin(plugin); + + config +} + +pub const MEDIA_POOL_CFG_FILENAME: &'static str = "/etc/proxmox-backup/media-pool.cfg"; +pub const MEDIA_POOL_CFG_LOCKFILE: &'static str = "/etc/proxmox-backup/.media-pool.lck"; + +pub fn lock() -> Result { + open_file_locked(MEDIA_POOL_CFG_LOCKFILE, std::time::Duration::new(10, 0), true) +} + +pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> { + + let content = proxmox::tools::fs::file_read_optional_string(MEDIA_POOL_CFG_FILENAME)?; + let content = content.unwrap_or(String::from("")); + + let digest = openssl::sha::sha256(content.as_bytes()); + let data = CONFIG.parse(MEDIA_POOL_CFG_FILENAME, &content)?; + Ok((data, digest)) +} + +pub fn save_config(config: &SectionConfigData) -> Result<(), Error> { + let raw = CONFIG.write(MEDIA_POOL_CFG_FILENAME, &config)?; + + let backup_user = crate::backup::backup_user()?; + let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640); + // set the correct owner/group/permissions while saving file + // owner(rw) = root, group(r)= backup + let options = CreateOptions::new() + .perm(mode) + .owner(nix::unistd::ROOT) + .group(backup_user.gid); + + replace_file(MEDIA_POOL_CFG_FILENAME, raw.as_bytes(), options)?; + + Ok(()) +} + +// shell completion helper + +/// List existing pool names +pub fn complete_pool_name(_arg: &str, _param: &HashMap) -> Vec { + match config() { + Ok((data, _digest)) => data.sections.iter().map(|(id, _)| id.to_string()).collect(), + Err(_) => return vec![], + } +}