diff --git a/pbs-api-types/src/lib.rs b/pbs-api-types/src/lib.rs index 0aa9374c..427b2d9f 100644 --- a/pbs-api-types/src/lib.rs +++ b/pbs-api-types/src/lib.rs @@ -61,6 +61,9 @@ pub mod file_restore; mod remote; pub use remote::*; +mod tape; +pub use tape::*; + #[rustfmt::skip] #[macro_use] mod local_macros { diff --git a/pbs-api-types/src/tape/changer.rs b/pbs-api-types/src/tape/changer.rs new file mode 100644 index 00000000..24f529df --- /dev/null +++ b/pbs-api-types/src/tape/changer.rs @@ -0,0 +1,135 @@ +//! Types for tape changer API + +use serde::{Deserialize, Serialize}; + +use proxmox::api::{ + api, + schema::{ + Schema, + ApiStringFormat, + ArraySchema, + IntegerSchema, + StringSchema, + }, +}; + +use crate::{ + PROXMOX_SAFE_ID_FORMAT, + OptionalDeviceIdentification, +}; + +pub const CHANGER_NAME_SCHEMA: Schema = StringSchema::new("Tape Changer Identifier.") + .format(&PROXMOX_SAFE_ID_FORMAT) + .min_length(3) + .max_length(32) + .schema(); + +pub const SCSI_CHANGER_PATH_SCHEMA: Schema = StringSchema::new( + "Path to Linux generic SCSI device (e.g. '/dev/sg4')") + .schema(); + +pub const MEDIA_LABEL_SCHEMA: Schema = StringSchema::new("Media Label/Barcode.") + .format(&PROXMOX_SAFE_ID_FORMAT) + .min_length(2) + .max_length(32) + .schema(); + +pub const SLOT_ARRAY_SCHEMA: Schema = ArraySchema::new( + "Slot list.", &IntegerSchema::new("Slot number") + .minimum(1) + .schema()) + .schema(); + +pub const EXPORT_SLOT_LIST_SCHEMA: Schema = StringSchema::new("\ +A list of slot numbers, comma separated. Those slots are reserved for +Import/Export, i.e. any media in those slots are considered to be +'offline'. +") +.format(&ApiStringFormat::PropertyString(&SLOT_ARRAY_SCHEMA)) +.schema(); + +#[api( + properties: { + name: { + schema: CHANGER_NAME_SCHEMA, + }, + path: { + schema: SCSI_CHANGER_PATH_SCHEMA, + }, + "export-slots": { + schema: EXPORT_SLOT_LIST_SCHEMA, + optional: true, + }, + }, +)] +#[derive(Serialize,Deserialize)] +#[serde(rename_all = "kebab-case")] +/// SCSI tape changer +pub struct ScsiTapeChanger { + pub name: String, + pub path: String, + #[serde(skip_serializing_if="Option::is_none")] + pub export_slots: Option, +} + +#[api( + properties: { + config: { + type: ScsiTapeChanger, + }, + info: { + type: OptionalDeviceIdentification, + }, + }, +)] +#[derive(Serialize,Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Changer config with optional device identification attributes +pub struct ChangerListEntry { + #[serde(flatten)] + pub config: ScsiTapeChanger, + #[serde(flatten)] + pub info: OptionalDeviceIdentification, +} + +#[api()] +#[derive(Serialize,Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Mtx Entry Kind +pub enum MtxEntryKind { + /// Drive + Drive, + /// Slot + Slot, + /// Import/Export Slot + ImportExport, +} + +#[api( + properties: { + "entry-kind": { + type: MtxEntryKind, + }, + "label-text": { + schema: MEDIA_LABEL_SCHEMA, + optional: true, + }, + }, +)] +#[derive(Serialize,Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Mtx Status Entry +pub struct MtxStatusEntry { + pub entry_kind: MtxEntryKind, + /// The ID of the slot or drive + pub entry_id: u64, + /// The media label (volume tag) if the slot/drive is full + #[serde(skip_serializing_if="Option::is_none")] + pub label_text: Option, + /// The slot the drive was loaded from + #[serde(skip_serializing_if="Option::is_none")] + pub loaded_slot: Option, + /// The current state of the drive + #[serde(skip_serializing_if="Option::is_none")] + pub state: Option, +} diff --git a/pbs-api-types/src/tape/device.rs b/pbs-api-types/src/tape/device.rs new file mode 100644 index 00000000..368a0015 --- /dev/null +++ b/pbs-api-types/src/tape/device.rs @@ -0,0 +1,55 @@ +use ::serde::{Deserialize, Serialize}; + +use proxmox::api::api; + +#[api()] +#[derive(Serialize,Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Optional Device Identification Attributes +pub struct OptionalDeviceIdentification { + /// Vendor (autodetected) + #[serde(skip_serializing_if="Option::is_none")] + pub vendor: Option, + /// Model (autodetected) + #[serde(skip_serializing_if="Option::is_none")] + pub model: Option, + /// Serial number (autodetected) + #[serde(skip_serializing_if="Option::is_none")] + pub serial: Option, +} + +#[api()] +#[derive(Debug,Serialize,Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Kind of device +pub enum DeviceKind { + /// Tape changer (Autoloader, Robot) + Changer, + /// Normal SCSI tape device + Tape, +} + +#[api( + properties: { + kind: { + type: DeviceKind, + }, + }, +)] +#[derive(Debug,Serialize,Deserialize)] +/// Tape device information +pub struct TapeDeviceInfo { + pub kind: DeviceKind, + /// Path to the linux device node + pub path: String, + /// Serial number (autodetected) + pub serial: String, + /// Vendor (autodetected) + pub vendor: String, + /// Model (autodetected) + pub model: String, + /// Device major number + pub major: u32, + /// Device minor number + pub minor: u32, +} diff --git a/pbs-api-types/src/tape/drive.rs b/pbs-api-types/src/tape/drive.rs new file mode 100644 index 00000000..3e207a99 --- /dev/null +++ b/pbs-api-types/src/tape/drive.rs @@ -0,0 +1,285 @@ +//! Types for tape drive API +use std::convert::TryFrom; + +use anyhow::{bail, Error}; +use serde::{Deserialize, Serialize}; + +use proxmox::api::{ + api, + schema::{Schema, IntegerSchema, StringSchema, Updater}, +}; + +use crate::{ + PROXMOX_SAFE_ID_FORMAT, + CHANGER_NAME_SCHEMA, + OptionalDeviceIdentification, +}; + +pub const DRIVE_NAME_SCHEMA: Schema = StringSchema::new("Drive Identifier.") + .format(&PROXMOX_SAFE_ID_FORMAT) + .min_length(3) + .max_length(32) + .schema(); + +pub const LTO_DRIVE_PATH_SCHEMA: Schema = StringSchema::new( + "The path to a LTO SCSI-generic tape device (i.e. '/dev/sg0')") + .schema(); + +pub const CHANGER_DRIVENUM_SCHEMA: Schema = IntegerSchema::new( + "Associated changer drive number (requires option changer)") + .minimum(0) + .maximum(255) + .default(0) + .schema(); + +#[api( + properties: { + name: { + schema: DRIVE_NAME_SCHEMA, + } + } +)] +#[derive(Serialize,Deserialize)] +/// Simulate tape drives (only for test and debug) +#[serde(rename_all = "kebab-case")] +pub struct VirtualTapeDrive { + pub name: String, + /// Path to directory + pub path: String, + /// Virtual tape size + #[serde(skip_serializing_if="Option::is_none")] + pub max_size: Option, +} + +#[api( + properties: { + name: { + schema: DRIVE_NAME_SCHEMA, + }, + path: { + schema: LTO_DRIVE_PATH_SCHEMA, + }, + changer: { + schema: CHANGER_NAME_SCHEMA, + optional: true, + }, + "changer-drivenum": { + schema: CHANGER_DRIVENUM_SCHEMA, + optional: true, + }, + } +)] +#[derive(Serialize,Deserialize,Updater)] +#[serde(rename_all = "kebab-case")] +/// Lto SCSI tape driver +pub struct LtoTapeDrive { + #[updater(skip)] + pub name: String, + pub path: String, + #[serde(skip_serializing_if="Option::is_none")] + pub changer: Option, + #[serde(skip_serializing_if="Option::is_none")] + pub changer_drivenum: Option, +} + +#[api( + properties: { + config: { + type: LtoTapeDrive, + }, + info: { + type: OptionalDeviceIdentification, + }, + }, +)] +#[derive(Serialize,Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Drive list entry +pub struct DriveListEntry { + #[serde(flatten)] + pub config: LtoTapeDrive, + #[serde(flatten)] + pub info: OptionalDeviceIdentification, + /// the state of the drive if locked + #[serde(skip_serializing_if="Option::is_none")] + pub state: Option, +} + +#[api()] +#[derive(Serialize,Deserialize)] +/// Medium auxiliary memory attributes (MAM) +pub struct MamAttribute { + /// Attribute id + pub id: u16, + /// Attribute name + pub name: String, + /// Attribute value + pub value: String, +} + +#[api()] +#[derive(Serialize,Deserialize,Copy,Clone,Debug)] +pub enum TapeDensity { + /// Unknown (no media loaded) + Unknown, + /// LTO1 + LTO1, + /// LTO2 + LTO2, + /// LTO3 + LTO3, + /// LTO4 + LTO4, + /// LTO5 + LTO5, + /// LTO6 + LTO6, + /// LTO7 + LTO7, + /// LTO7M8 + LTO7M8, + /// LTO8 + LTO8, +} + +impl TryFrom for TapeDensity { + type Error = Error; + + fn try_from(value: u8) -> Result { + let density = match value { + 0x00 => TapeDensity::Unknown, + 0x40 => TapeDensity::LTO1, + 0x42 => TapeDensity::LTO2, + 0x44 => TapeDensity::LTO3, + 0x46 => TapeDensity::LTO4, + 0x58 => TapeDensity::LTO5, + 0x5a => TapeDensity::LTO6, + 0x5c => TapeDensity::LTO7, + 0x5d => TapeDensity::LTO7M8, + 0x5e => TapeDensity::LTO8, + _ => bail!("unknown tape density code 0x{:02x}", value), + }; + Ok(density) + } +} + +#[api( + properties: { + density: { + type: TapeDensity, + optional: true, + }, + }, +)] +#[derive(Serialize,Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Drive/Media status for Lto SCSI drives. +/// +/// Media related data is optional - only set if there is a medium +/// loaded. +pub struct LtoDriveAndMediaStatus { + /// Vendor + pub vendor: String, + /// Product + pub product: String, + /// Revision + pub revision: String, + /// Block size (0 is variable size) + pub blocksize: u32, + /// Compression enabled + pub compression: bool, + /// Drive buffer mode + pub buffer_mode: u8, + /// Tape density + pub density: TapeDensity, + /// Media is write protected + #[serde(skip_serializing_if="Option::is_none")] + pub write_protect: Option, + /// Tape Alert Flags + #[serde(skip_serializing_if="Option::is_none")] + pub alert_flags: Option, + /// Current file number + #[serde(skip_serializing_if="Option::is_none")] + pub file_number: Option, + /// Current block number + #[serde(skip_serializing_if="Option::is_none")] + pub block_number: Option, + /// Medium Manufacture Date (epoch) + #[serde(skip_serializing_if="Option::is_none")] + pub manufactured: Option, + /// Total Bytes Read in Medium Life + #[serde(skip_serializing_if="Option::is_none")] + pub bytes_read: Option, + /// Total Bytes Written in Medium Life + #[serde(skip_serializing_if="Option::is_none")] + pub bytes_written: Option, + /// Number of mounts for the current volume (i.e., Thread Count) + #[serde(skip_serializing_if="Option::is_none")] + pub volume_mounts: Option, + /// Count of the total number of times the medium has passed over + /// the head. + #[serde(skip_serializing_if="Option::is_none")] + pub medium_passes: Option, + /// Estimated tape wearout factor (assuming max. 16000 end-to-end passes) + #[serde(skip_serializing_if="Option::is_none")] + pub medium_wearout: Option, +} + +#[api()] +/// Volume statistics from SCSI log page 17h +#[derive(Default, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Lp17VolumeStatistics { + /// Volume mounts (thread count) + pub volume_mounts: u64, + /// Total data sets written + pub volume_datasets_written: u64, + /// Write retries + pub volume_recovered_write_data_errors: u64, + /// Total unrecovered write errors + pub volume_unrecovered_write_data_errors: u64, + /// Total suspended writes + pub volume_write_servo_errors: u64, + /// Total fatal suspended writes + pub volume_unrecovered_write_servo_errors: u64, + /// Total datasets read + pub volume_datasets_read: u64, + /// Total read retries + pub volume_recovered_read_errors: u64, + /// Total unrecovered read errors + pub volume_unrecovered_read_errors: u64, + /// Last mount unrecovered write errors + pub last_mount_unrecovered_write_errors: u64, + /// Last mount unrecovered read errors + pub last_mount_unrecovered_read_errors: u64, + /// Last mount bytes written + pub last_mount_bytes_written: u64, + /// Last mount bytes read + pub last_mount_bytes_read: u64, + /// Lifetime bytes written + pub lifetime_bytes_written: u64, + /// Lifetime bytes read + pub lifetime_bytes_read: u64, + /// Last load write compression ratio + pub last_load_write_compression_ratio: u64, + /// Last load read compression ratio + pub last_load_read_compression_ratio: u64, + /// Medium mount time + pub medium_mount_time: u64, + /// Medium ready time + pub medium_ready_time: u64, + /// Total native capacity + pub total_native_capacity: u64, + /// Total used native capacity + pub total_used_native_capacity: u64, + /// Write protect + pub write_protect: bool, + /// Volume is WORM + pub worm: bool, + /// Beginning of medium passes + pub beginning_of_medium_passes: u64, + /// Middle of medium passes + pub middle_of_tape_passes: u64, + /// Volume serial number + pub serial: String, +} diff --git a/pbs-api-types/src/tape/media.rs b/pbs-api-types/src/tape/media.rs new file mode 100644 index 00000000..4e301c98 --- /dev/null +++ b/pbs-api-types/src/tape/media.rs @@ -0,0 +1,182 @@ +use ::serde::{Deserialize, Serialize}; + +use proxmox::{ + api::{api, schema::*}, + tools::Uuid, +}; + +use crate::{ + UUID_FORMAT, + MediaStatus, + MediaLocation, +}; + +pub const MEDIA_SET_UUID_SCHEMA: Schema = + StringSchema::new("MediaSet Uuid (We use the all-zero Uuid to reseve an empty media for a specific pool).") + .format(&UUID_FORMAT) + .schema(); + +pub const MEDIA_UUID_SCHEMA: Schema = + StringSchema::new("Media Uuid.") + .format(&UUID_FORMAT) + .schema(); + +#[api( + properties: { + "media-set-uuid": { + schema: MEDIA_SET_UUID_SCHEMA, + }, + }, +)] +#[derive(Serialize,Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Media Set list entry +pub struct MediaSetListEntry { + /// Media set name + pub media_set_name: String, + pub media_set_uuid: Uuid, + /// MediaSet creation time stamp + pub media_set_ctime: i64, + /// Media Pool + pub pool: String, +} + +#[api( + properties: { + location: { + type: MediaLocation, + }, + status: { + type: MediaStatus, + }, + uuid: { + schema: MEDIA_UUID_SCHEMA, + }, + "media-set-uuid": { + schema: MEDIA_SET_UUID_SCHEMA, + optional: true, + }, + }, +)] +#[derive(Serialize,Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Media list entry +pub struct MediaListEntry { + /// Media label text (or Barcode) + pub label_text: String, + pub uuid: Uuid, + /// Creation time stamp + pub ctime: i64, + pub location: MediaLocation, + pub status: MediaStatus, + /// Expired flag + pub expired: bool, + /// Catalog status OK + pub catalog: bool, + /// Media set name + #[serde(skip_serializing_if="Option::is_none")] + pub media_set_name: Option, + #[serde(skip_serializing_if="Option::is_none")] + pub media_set_uuid: Option, + /// Media set seq_nr + #[serde(skip_serializing_if="Option::is_none")] + pub seq_nr: Option, + /// MediaSet creation time stamp + #[serde(skip_serializing_if="Option::is_none")] + pub media_set_ctime: Option, + /// Media Pool + #[serde(skip_serializing_if="Option::is_none")] + pub pool: Option, +} + +#[api( + properties: { + uuid: { + schema: MEDIA_UUID_SCHEMA, + }, + "media-set-uuid": { + schema: MEDIA_SET_UUID_SCHEMA, + optional: true, + }, + }, +)] +#[derive(Serialize,Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Media label info +pub struct MediaIdFlat { + /// Unique ID + pub uuid: Uuid, + /// Media label text (or Barcode) + pub label_text: String, + /// Creation time stamp + pub ctime: i64, + // All MediaSet properties are optional here + /// MediaSet Pool + #[serde(skip_serializing_if="Option::is_none")] + pub pool: Option, + #[serde(skip_serializing_if="Option::is_none")] + pub media_set_uuid: Option, + /// MediaSet media sequence number + #[serde(skip_serializing_if="Option::is_none")] + pub seq_nr: Option, + /// MediaSet Creation time stamp + #[serde(skip_serializing_if="Option::is_none")] + pub media_set_ctime: Option, + /// Encryption key fingerprint + #[serde(skip_serializing_if="Option::is_none")] + pub encryption_key_fingerprint: Option, +} + +#[api( + properties: { + uuid: { + schema: MEDIA_UUID_SCHEMA, + optional: true, + }, + }, +)] +#[derive(Serialize,Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Label with optional Uuid +pub struct LabelUuidMap { + /// Changer label text (or Barcode) + pub label_text: String, + /// Associated Uuid (if any) + pub uuid: Option, +} + +#[api( + properties: { + uuid: { + schema: MEDIA_UUID_SCHEMA, + }, + "media-set-uuid": { + schema: MEDIA_SET_UUID_SCHEMA, + }, + }, +)] +#[derive(Serialize,Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Media content list entry +pub struct MediaContentEntry { + /// Media label text (or Barcode) + pub label_text: String, + /// Media Uuid + pub uuid: Uuid, + /// Media set name + pub media_set_name: String, + /// Media set uuid + pub media_set_uuid: Uuid, + /// MediaSet Creation time stamp + pub media_set_ctime: i64, + /// Media set seq_nr + pub seq_nr: u64, + /// Media Pool + pub pool: String, + /// Datastore Name + pub store: String, + /// Backup snapshot + pub snapshot: String, + /// Snapshot creation time (epoch) + pub backup_time: i64, +} diff --git a/pbs-api-types/src/tape/media_location.rs b/pbs-api-types/src/tape/media_location.rs new file mode 100644 index 00000000..a917c609 --- /dev/null +++ b/pbs-api-types/src/tape/media_location.rs @@ -0,0 +1,91 @@ +use anyhow::{bail, Error}; + +use proxmox::api::{ + schema::{ + Schema, + StringSchema, + ApiStringFormat, + parse_simple_value, + }, +}; + +use crate::{ + PROXMOX_SAFE_ID_FORMAT, + CHANGER_NAME_SCHEMA, +}; + +pub const VAULT_NAME_SCHEMA: Schema = StringSchema::new("Vault name.") + .format(&PROXMOX_SAFE_ID_FORMAT) + .min_length(3) + .max_length(32) + .schema(); + +#[derive(Debug, PartialEq, Clone)] +/// 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), +} + +proxmox::forward_deserialize_to_from_str!(MediaLocation); +proxmox::forward_serialize_to_display!(MediaLocation); + +impl proxmox::api::schema::ApiType for MediaLocation { + const API_SCHEMA: Schema = StringSchema::new( + "Media location (e.g. 'offline', 'online-', 'vault-')") + .format(&ApiStringFormat::VerifyFn(|text| { + let location: MediaLocation = text.parse()?; + match location { + MediaLocation::Online(ref changer) => { + parse_simple_value(changer, &CHANGER_NAME_SCHEMA)?; + } + MediaLocation::Vault(ref vault) => { + parse_simple_value(vault, &VAULT_NAME_SCHEMA)?; + } + MediaLocation::Offline => { /* OK */} + } + Ok(()) + })) + .schema(); +} + + +impl std::fmt::Display for MediaLocation { + + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MediaLocation::Offline => { + write!(f, "offline") + } + MediaLocation::Online(changer) => { + write!(f, "online-{}", changer) + } + MediaLocation::Vault(vault) => { + write!(f, "vault-{}", vault) + } + } + } +} + +impl std::str::FromStr for MediaLocation { + type Err = Error; + + fn from_str(s: &str) -> Result { + if s == "offline" { + return Ok(MediaLocation::Offline); + } + if let Some(changer) = s.strip_prefix("online-") { + return Ok(MediaLocation::Online(changer.to_string())); + } + if let Some(vault) = s.strip_prefix("vault-") { + return Ok(MediaLocation::Vault(vault.to_string())); + } + + bail!("MediaLocation parse error"); + } +} diff --git a/pbs-api-types/src/tape/media_pool.rs b/pbs-api-types/src/tape/media_pool.rs new file mode 100644 index 00000000..53e46788 --- /dev/null +++ b/pbs-api-types/src/tape/media_pool.rs @@ -0,0 +1,161 @@ +//! 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 std::str::FromStr; + +use anyhow::Error; +use serde::{Deserialize, Serialize}; + +use proxmox::api::{ + api, + schema::{Schema, StringSchema, ApiStringFormat, Updater}, +}; + +use pbs_systemd::time::{parse_calendar_event, parse_time_span, CalendarEvent, TimeSpan}; + +use crate::{ + PROXMOX_SAFE_ID_FORMAT, + SINGLE_LINE_COMMENT_FORMAT, + SINGLE_LINE_COMMENT_SCHEMA, + TAPE_ENCRYPTION_KEY_FINGERPRINT_SCHEMA, +}; + +pub const MEDIA_POOL_NAME_SCHEMA: Schema = StringSchema::new("Media pool name.") + .format(&PROXMOX_SAFE_ID_FORMAT) + .min_length(2) + .max_length(32) + .schema(); + +pub const MEDIA_SET_NAMING_TEMPLATE_SCHEMA: Schema = StringSchema::new( + "Media set naming template (may contain strftime() time format specifications).") + .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 ('continue', 'always', or a calendar event).") + .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 ('overwrite', 'keep', or time span).") + .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, + }, + 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, + }, + encrypt: { + schema: TAPE_ENCRYPTION_KEY_FINGERPRINT_SCHEMA, + optional: true, + }, + comment: { + optional: true, + schema: SINGLE_LINE_COMMENT_SCHEMA, + }, + }, +)] +#[derive(Serialize,Deserialize,Updater)] +/// Media pool configuration +pub struct MediaPoolConfig { + /// The pool name + #[updater(skip)] + pub name: 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 "%c") + /// + /// The template is UTF8 text, and can include strftime time + /// format specifications. + #[serde(skip_serializing_if="Option::is_none")] + pub template: Option, + /// Encryption key fingerprint + /// + /// If set, encrypt all data using the specified key. + #[serde(skip_serializing_if="Option::is_none")] + pub encrypt: Option, + #[serde(skip_serializing_if="Option::is_none")] + pub comment: Option, +} diff --git a/pbs-api-types/src/tape/media_status.rs b/pbs-api-types/src/tape/media_status.rs new file mode 100644 index 00000000..5a3bff96 --- /dev/null +++ b/pbs-api-types/src/tape/media_status.rs @@ -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, +} diff --git a/pbs-api-types/src/tape/mod.rs b/pbs-api-types/src/tape/mod.rs new file mode 100644 index 00000000..a77be7f7 --- /dev/null +++ b/pbs-api-types/src/tape/mod.rs @@ -0,0 +1,94 @@ +//! Types for tape backup API + +mod device; +pub use device::*; + +mod changer; +pub use changer::*; + +mod drive; +pub use drive::*; + +mod media_pool; +pub use media_pool::*; + +mod media_status; +pub use media_status::*; + +mod media_location; + +pub use media_location::*; + +mod media; +pub use media::*; + +use ::serde::{Deserialize, Serialize}; + +use proxmox::api::api; +use proxmox::api::schema::{Schema, StringSchema, ApiStringFormat}; +use proxmox::tools::Uuid; + +use proxmox::const_regex; + +use crate::{ + FINGERPRINT_SHA256_FORMAT, BACKUP_ID_SCHEMA, BACKUP_TYPE_SCHEMA, +}; + +const_regex!{ + pub TAPE_RESTORE_SNAPSHOT_REGEX = concat!(r"^", PROXMOX_SAFE_ID_REGEX_STR!(), r":", SNAPSHOT_PATH_REGEX_STR!(), r"$"); +} + +pub const TAPE_RESTORE_SNAPSHOT_FORMAT: ApiStringFormat = + ApiStringFormat::Pattern(&TAPE_RESTORE_SNAPSHOT_REGEX); + +pub const TAPE_ENCRYPTION_KEY_FINGERPRINT_SCHEMA: Schema = StringSchema::new( + "Tape encryption key fingerprint (sha256)." +) + .format(&FINGERPRINT_SHA256_FORMAT) + .schema(); + +pub const TAPE_RESTORE_SNAPSHOT_SCHEMA: Schema = StringSchema::new( + "A snapshot in the format: 'store:type/id/time") + .format(&TAPE_RESTORE_SNAPSHOT_FORMAT) + .type_text("store:type/id/time") + .schema(); + +#[api( + properties: { + pool: { + schema: MEDIA_POOL_NAME_SCHEMA, + optional: true, + }, + "label-text": { + schema: MEDIA_LABEL_SCHEMA, + optional: true, + }, + "media": { + schema: MEDIA_UUID_SCHEMA, + optional: true, + }, + "media-set": { + schema: MEDIA_SET_UUID_SCHEMA, + optional: true, + }, + "backup-type": { + schema: BACKUP_TYPE_SCHEMA, + optional: true, + }, + "backup-id": { + schema: BACKUP_ID_SCHEMA, + optional: true, + }, + }, +)] +#[derive(Serialize,Deserialize)] +#[serde(rename_all="kebab-case")] +/// Content list filter parameters +pub struct MediaContentListFilter { + pub pool: Option, + pub label_text: Option, + pub media: Option, + pub media_set: Option, + pub backup_type: Option, + pub backup_id: Option, +}