diff --git a/pbs-api-types/Cargo.toml b/pbs-api-types/Cargo.toml index cd3a7073..2463d69d 100644 --- a/pbs-api-types/Cargo.toml +++ b/pbs-api-types/Cargo.toml @@ -16,3 +16,4 @@ serde = { version = "1.0", features = ["derive"] } proxmox = { version = "0.11.5", default-features = false, features = [ "api-macro" ] } pbs-systemd = { path = "../pbs-systemd" } +pbs-tools = { path = "../pbs-tools" } diff --git a/pbs-api-types/src/crypto.rs b/pbs-api-types/src/crypto.rs new file mode 100644 index 00000000..7b36e85f --- /dev/null +++ b/pbs-api-types/src/crypto.rs @@ -0,0 +1,57 @@ +use std::fmt::{self, Display}; + +use anyhow::Error; +use serde::{Deserialize, Serialize}; + +use proxmox::api::api; + +use pbs_tools::format::{as_fingerprint, bytes_as_fingerprint}; + +#[api(default: "encrypt")] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +/// Defines whether data is encrypted (using an AEAD cipher), only signed, or neither. +pub enum CryptMode { + /// Don't encrypt. + None, + /// Encrypt. + Encrypt, + /// Only sign. + SignOnly, +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone, Deserialize, Serialize)] +#[serde(transparent)] +/// 32-byte fingerprint, usually calculated with SHA256. +pub struct Fingerprint { + #[serde(with = "bytes_as_fingerprint")] + bytes: [u8; 32], +} + +impl Fingerprint { + pub fn new(bytes: [u8; 32]) -> Self { + Self { bytes } + } + pub fn bytes(&self) -> &[u8; 32] { + &self.bytes + } +} + +/// Display as short key ID +impl Display for Fingerprint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", as_fingerprint(&self.bytes[0..8])) + } +} + +impl std::str::FromStr for Fingerprint { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mut tmp = s.to_string(); + tmp.retain(|c| c != ':'); + let bytes = proxmox::tools::hex_to_digest(&tmp)?; + Ok(Fingerprint::new(bytes)) + } +} + diff --git a/pbs-api-types/src/lib.rs b/pbs-api-types/src/lib.rs index 8e2d1a7c..2d15e92e 100644 --- a/pbs-api-types/src/lib.rs +++ b/pbs-api-types/src/lib.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use proxmox::api::api; -use proxmox::api::schema::{ApiStringFormat, Schema, StringSchema}; +use proxmox::api::schema::{ApiStringFormat, EnumEntry, IntegerSchema, Schema, StringSchema}; use proxmox::const_regex; use proxmox::{IPRE, IPRE_BRACKET, IPV4OCTET, IPV4RE, IPV6H16, IPV6LS32, IPV6RE}; @@ -43,6 +43,9 @@ pub use userid::{PROXMOX_GROUP_ID_SCHEMA, PROXMOX_TOKEN_ID_SCHEMA, PROXMOX_TOKEN pub mod upid; pub use upid::UPID; +mod crypto; +pub use crypto::{CryptMode, Fingerprint}; + #[rustfmt::skip] #[macro_use] mod local_macros { @@ -115,6 +118,26 @@ pub const CIDR_V4_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&CIDR_V4_RE pub const CIDR_V6_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&CIDR_V6_REGEX); pub const CIDR_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&CIDR_REGEX); +pub const BACKUP_ID_SCHEMA: Schema = StringSchema::new("Backup ID.") + .format(&BACKUP_ID_FORMAT) + .schema(); +pub const BACKUP_TYPE_SCHEMA: Schema = StringSchema::new("Backup type.") + .format(&ApiStringFormat::Enum(&[ + EnumEntry::new("vm", "Virtual Machine Backup"), + EnumEntry::new("ct", "Container Backup"), + EnumEntry::new("host", "Host Backup"), + ])) + .schema(); +pub const BACKUP_TIME_SCHEMA: Schema = IntegerSchema::new("Backup time (Unix epoch.)") + .minimum(1_547_797_308) + .schema(); + +pub const DATASTORE_SCHEMA: Schema = StringSchema::new("Datastore name.") + .format(&PROXMOX_SAFE_ID_FORMAT) + .min_length(3) + .max_length(32) + .schema(); + pub const FINGERPRINT_SHA256_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&FINGERPRINT_SHA256_REGEX); @@ -197,3 +220,207 @@ pub const CHUNK_DIGEST_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&SHA25 pub const PASSWORD_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&PASSWORD_REGEX); pub const UUID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&UUID_REGEX); + +pub const BACKUP_ARCHIVE_NAME_SCHEMA: Schema = StringSchema::new("Backup archive name.") + .format(&PROXMOX_SAFE_ID_FORMAT) + .schema(); + +// Complex type definitions + +#[api( + properties: { + "filename": { + schema: BACKUP_ARCHIVE_NAME_SCHEMA, + }, + "crypt-mode": { + type: CryptMode, + optional: true, + }, + }, +)] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Basic information about archive files inside a backup snapshot. +pub struct BackupContent { + pub filename: String, + /// Info if file is encrypted, signed, or neither. + #[serde(skip_serializing_if = "Option::is_none")] + pub crypt_mode: Option, + /// Archive size (from backup manifest). + #[serde(skip_serializing_if = "Option::is_none")] + pub size: Option, +} + +#[api()] +#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +/// Result of a verify operation. +pub enum VerifyState { + /// Verification was successful + Ok, + /// Verification reported one or more errors + Failed, +} + +#[api( + properties: { + upid: { + type: UPID, + }, + state: { + type: VerifyState, + }, + }, +)] +#[derive(Serialize, Deserialize)] +/// Task properties. +pub struct SnapshotVerifyState { + /// UPID of the verify task + pub upid: UPID, + /// State of the verification. Enum. + pub state: VerifyState, +} + +#[api( + properties: { + "backup-type": { + schema: BACKUP_TYPE_SCHEMA, + }, + "backup-id": { + schema: BACKUP_ID_SCHEMA, + }, + "backup-time": { + schema: BACKUP_TIME_SCHEMA, + }, + comment: { + schema: SINGLE_LINE_COMMENT_SCHEMA, + optional: true, + }, + verification: { + type: SnapshotVerifyState, + optional: true, + }, + fingerprint: { + type: String, + optional: true, + }, + files: { + items: { + schema: BACKUP_ARCHIVE_NAME_SCHEMA + }, + }, + owner: { + type: Authid, + optional: true, + }, + }, +)] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Basic information about backup snapshot. +pub struct SnapshotListItem { + pub backup_type: String, // enum + pub backup_id: String, + pub backup_time: i64, + /// The first line from manifest "notes" + #[serde(skip_serializing_if = "Option::is_none")] + pub comment: Option, + /// The result of the last run verify task + #[serde(skip_serializing_if = "Option::is_none")] + pub verification: Option, + /// Fingerprint of encryption key + #[serde(skip_serializing_if = "Option::is_none")] + pub fingerprint: Option, + /// List of contained archive files. + pub files: Vec, + /// Overall snapshot size (sum of all archive sizes). + #[serde(skip_serializing_if = "Option::is_none")] + pub size: Option, + /// The owner of the snapshots group + #[serde(skip_serializing_if = "Option::is_none")] + pub owner: Option, +} + +#[api( + properties: { + "backup-type": { + schema: BACKUP_TYPE_SCHEMA, + }, + "backup-id": { + schema: BACKUP_ID_SCHEMA, + }, + "last-backup": { + schema: BACKUP_TIME_SCHEMA, + }, + "backup-count": { + type: Integer, + }, + files: { + items: { + schema: BACKUP_ARCHIVE_NAME_SCHEMA + }, + }, + owner: { + type: Authid, + optional: true, + }, + }, +)] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Basic information about a backup group. +pub struct GroupListItem { + pub backup_type: String, // enum + pub backup_id: String, + pub last_backup: i64, + /// Number of contained snapshots + pub backup_count: u64, + /// List of contained archive files. + pub files: Vec, + /// The owner of group + #[serde(skip_serializing_if = "Option::is_none")] + pub owner: Option, +} + +#[api( + properties: { + store: { + schema: DATASTORE_SCHEMA, + }, + comment: { + optional: true, + schema: SINGLE_LINE_COMMENT_SCHEMA, + }, + }, +)] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Basic information about a datastore. +pub struct DataStoreListItem { + pub store: String, + pub comment: Option, +} + +#[api( + properties: { + "backup-type": { + schema: BACKUP_TYPE_SCHEMA, + }, + "backup-id": { + schema: BACKUP_ID_SCHEMA, + }, + "backup-time": { + schema: BACKUP_TIME_SCHEMA, + }, + }, +)] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Prune result. +pub struct PruneListItem { + pub backup_type: String, // enum + pub backup_id: String, + pub backup_time: i64, + /// Keep snapshot + pub keep: bool, +}