api-types: introduce BackupType enum and Group/Dir api types

The type is a real enum.

All are API types and implement Display and FromStr. The
ordering is the same as it is in pbs-datastore.

Also, they are now flattened into a few structs instead of
being copied manually.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2022-04-14 15:05:58 +02:00 committed by Thomas Lamprecht
parent b635dc3ee1
commit 32ea4b56a1
2 changed files with 253 additions and 38 deletions

View File

@ -1,3 +1,6 @@
use std::fmt;
use anyhow::{bail, format_err, Error};
use serde::{Deserialize, Serialize};
use proxmox_schema::{
@ -394,17 +397,244 @@ pub struct SnapshotVerifyState {
pub state: VerifyState,
}
#[api]
/// Backup types.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum BackupType {
/// Virtual machines.
Vm,
/// Containers.
Ct,
/// "Host" backups.
Host,
}
impl BackupType {
pub const fn as_str(&self) -> &'static str {
match self {
BackupType::Vm => "vm",
BackupType::Ct => "ct",
BackupType::Host => "host",
}
}
/// We used to have alphabetical ordering here when this was a string.
const fn order(self) -> u8 {
match self {
BackupType::Ct => 0,
BackupType::Host => 1,
BackupType::Vm => 2,
}
}
}
impl fmt::Display for BackupType {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self.as_str(), f)
}
}
impl std::str::FromStr for BackupType {
type Err = Error;
/// Parse a backup type.
fn from_str(ty: &str) -> Result<Self, Error> {
Ok(match ty {
"ct" => BackupType::Ct,
"host" => BackupType::Host,
"vm" => BackupType::Vm,
_ => bail!("invalid backup type {ty:?}"),
})
}
}
impl std::cmp::Ord for BackupType {
#[inline]
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.order().cmp(&other.order())
}
}
impl std::cmp::PartialOrd for BackupType {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[api(
properties: {
"backup-type": {
schema: BACKUP_TYPE_SCHEMA,
},
"backup-id": {
schema: BACKUP_ID_SCHEMA,
},
"backup-time": {
schema: BACKUP_TIME_SCHEMA,
},
"backup-type": { type: BackupType },
"backup-id": { schema: BACKUP_ID_SCHEMA },
},
)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
/// A backup group (without a data store).
pub struct BackupGroup {
/// Backup type.
#[serde(rename = "backup-type")]
pub ty: BackupType,
/// Backup id.
#[serde(rename = "backup-id")]
pub id: String,
}
impl BackupGroup {
pub fn new<T: Into<String>>(ty: BackupType, id: T) -> Self {
Self { ty, id: id.into() }
}
}
impl From<(BackupType, String)> for BackupGroup {
fn from(data: (BackupType, String)) -> Self {
Self {
ty: data.0,
id: data.1,
}
}
}
impl std::cmp::Ord for BackupGroup {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
let type_order = self.ty.cmp(&other.ty);
if type_order != std::cmp::Ordering::Equal {
return type_order;
}
// try to compare IDs numerically
let id_self = self.id.parse::<u64>();
let id_other = other.id.parse::<u64>();
match (id_self, id_other) {
(Ok(id_self), Ok(id_other)) => id_self.cmp(&id_other),
(Ok(_), Err(_)) => std::cmp::Ordering::Less,
(Err(_), Ok(_)) => std::cmp::Ordering::Greater,
_ => self.id.cmp(&other.id),
}
}
}
impl std::cmp::PartialOrd for BackupGroup {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for BackupGroup {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.ty, self.id)
}
}
impl std::str::FromStr for BackupGroup {
type Err = Error;
/// Parse a backup group.
///
/// This parses strings like `vm/100".
fn from_str(path: &str) -> Result<Self, Error> {
let cap = GROUP_PATH_REGEX
.captures(path)
.ok_or_else(|| format_err!("unable to parse backup group path '{}'", path))?;
Ok(Self {
ty: cap.get(1).unwrap().as_str().parse()?,
id: cap.get(2).unwrap().as_str().to_owned(),
})
}
}
#[api(
properties: {
"group": { type: BackupGroup },
"backup-time": { schema: BACKUP_TIME_SCHEMA },
},
)]
/// Uniquely identify a Backup (relative to data store)
///
/// We also call this a backup snaphost.
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct BackupDir {
/// Backup group.
#[serde(flatten)]
pub group: BackupGroup,
/// Backup timestamp unix epoch.
#[serde(rename = "backup-time")]
pub time: i64,
}
impl From<(BackupGroup, i64)> for BackupDir {
fn from(data: (BackupGroup, i64)) -> Self {
Self {
group: data.0,
time: data.1,
}
}
}
impl From<(BackupType, String, i64)> for BackupDir {
fn from(data: (BackupType, String, i64)) -> Self {
Self {
group: (data.0, data.1).into(),
time: data.2,
}
}
}
impl BackupDir {
pub fn with_rfc3339<T>(ty: BackupType, id: T, backup_time_string: &str) -> Result<Self, Error>
where
T: Into<String>,
{
let time = proxmox_time::parse_rfc3339(&backup_time_string)?;
let group = BackupGroup::new(ty, id.into());
Ok(Self { group, time })
}
pub fn ty(&self) -> BackupType {
self.group.ty
}
pub fn id(&self) -> &str {
&self.group.id
}
}
impl std::str::FromStr for BackupDir {
type Err = Error;
/// Parse a snapshot path.
///
/// This parses strings like `host/elsa/2020-06-15T05:18:33Z".
fn from_str(path: &str) -> Result<Self, Self::Err> {
let cap = SNAPSHOT_PATH_REGEX
.captures(path)
.ok_or_else(|| format_err!("unable to parse backup snapshot path '{}'", path))?;
BackupDir::with_rfc3339(
cap.get(1).unwrap().as_str().parse()?,
cap.get(2).unwrap().as_str(),
cap.get(3).unwrap().as_str(),
)
}
}
impl std::fmt::Display for BackupDir {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// FIXME: log error?
let time = proxmox_time::epoch_to_rfc3339_utc(self.time).map_err(|_| fmt::Error)?;
write!(f, "{}/{}", self.group, time)
}
}
#[api(
properties: {
"backup": { type: BackupDir },
comment: {
schema: SINGLE_LINE_COMMENT_SCHEMA,
optional: true,
@ -432,9 +662,8 @@ pub struct SnapshotVerifyState {
#[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,
#[serde(flatten)]
pub backup: BackupDir,
/// The first line from manifest "notes"
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
@ -459,15 +688,8 @@ pub struct SnapshotListItem {
#[api(
properties: {
"backup-type": {
schema: BACKUP_TYPE_SCHEMA,
},
"backup-id": {
schema: BACKUP_ID_SCHEMA,
},
"last-backup": {
schema: BACKUP_TIME_SCHEMA,
},
"backup": { type: BackupGroup },
"last-backup": { schema: BACKUP_TIME_SCHEMA },
"backup-count": {
type: Integer,
},
@ -486,8 +708,9 @@ pub struct SnapshotListItem {
#[serde(rename_all = "kebab-case")]
/// Basic information about a backup group.
pub struct GroupListItem {
pub backup_type: String, // enum
pub backup_id: String,
#[serde(flatten)]
pub backup: BackupGroup,
pub last_backup: i64,
/// Number of contained snapshots
pub backup_count: u64,
@ -503,24 +726,16 @@ pub struct GroupListItem {
#[api(
properties: {
"backup-type": {
schema: BACKUP_TYPE_SCHEMA,
},
"backup-id": {
schema: BACKUP_ID_SCHEMA,
},
"backup-time": {
schema: BACKUP_TIME_SCHEMA,
},
"backup": { type: BackupDir },
},
)]
#[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,
#[serde(flatten)]
pub backup: BackupDir,
/// Keep snapshot
pub keep: bool,
}

View File

@ -27,7 +27,7 @@ use serde::{Deserialize, Serialize};
use proxmox_schema::{api, const_regex, ApiStringFormat, Schema, StringSchema};
use proxmox_uuid::Uuid;
use crate::{BACKUP_ID_SCHEMA, BACKUP_TYPE_SCHEMA, FINGERPRINT_SHA256_FORMAT};
use crate::{BackupType, BACKUP_ID_SCHEMA, FINGERPRINT_SHA256_FORMAT};
const_regex! {
pub TAPE_RESTORE_SNAPSHOT_REGEX = concat!(r"^", PROXMOX_SAFE_ID_REGEX_STR!(), r":", SNAPSHOT_PATH_REGEX_STR!(), r"$");
@ -66,7 +66,7 @@ pub const TAPE_RESTORE_SNAPSHOT_SCHEMA: Schema =
optional: true,
},
"backup-type": {
schema: BACKUP_TYPE_SCHEMA,
type: BackupType,
optional: true,
},
"backup-id": {
@ -83,6 +83,6 @@ pub struct MediaContentListFilter {
pub label_text: Option<String>,
pub media: Option<Uuid>,
pub media_set: Option<Uuid>,
pub backup_type: Option<String>,
pub backup_type: Option<BackupType>,
pub backup_id: Option<String>,
}