api: datastore/namespace: return backup groups delete stats on remove

Add and optionally expose the backup group delete statistics by adding the
return type to the corresponding REST API endpoints.

Clients can opt-into the new behaviour by setting the new `error-on-protected`
flag to `false` when calling the api endpoints, which results in removal not
erroring out when encountering protected snapshots.

The default value for the flag remains `true` for now, to remain backwards
compatible with older clients expecting this behaviour.

Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
FG: reworded commit message slightly
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
This commit is contained in:
Christian Ebner 2024-11-11 16:43:51 +01:00 committed by Fabian Grünbichler
parent 5462d9d44d
commit 4e50ef5193
4 changed files with 61 additions and 25 deletions

View File

@ -492,16 +492,22 @@ impl DataStore {
/// ///
/// Does *not* descends into child-namespaces and doesn't remoes the namespace itself either. /// Does *not* descends into child-namespaces and doesn't remoes the namespace itself either.
/// ///
/// Returns true if all the groups were removed, and false if some were protected. /// Returns a tuple with the first item being true if all the groups were removed, and false if some were protected.
pub fn remove_namespace_groups(self: &Arc<Self>, ns: &BackupNamespace) -> Result<bool, Error> { /// The second item returns the remove statistics.
pub fn remove_namespace_groups(
self: &Arc<Self>,
ns: &BackupNamespace,
) -> Result<(bool, BackupGroupDeleteStats), Error> {
// FIXME: locking? The single groups/snapshots are already protected, so may not be // FIXME: locking? The single groups/snapshots are already protected, so may not be
// necessary (depends on what we all allow to do with namespaces) // necessary (depends on what we all allow to do with namespaces)
log::info!("removing all groups in namespace {}:/{ns}", self.name()); log::info!("removing all groups in namespace {}:/{ns}", self.name());
let mut removed_all_groups = true; let mut removed_all_groups = true;
let mut stats = BackupGroupDeleteStats::default();
for group in self.iter_backup_groups(ns.to_owned())? { for group in self.iter_backup_groups(ns.to_owned())? {
let delete_stats = group?.destroy()?; let delete_stats = group?.destroy()?;
stats.add(&delete_stats);
removed_all_groups = removed_all_groups && delete_stats.all_removed(); removed_all_groups = removed_all_groups && delete_stats.all_removed();
} }
@ -518,7 +524,7 @@ impl DataStore {
} }
} }
Ok(removed_all_groups) Ok((removed_all_groups, stats))
} }
/// Remove a complete backup namespace optionally including all it's, and child namespaces', /// Remove a complete backup namespace optionally including all it's, and child namespaces',
@ -530,13 +536,15 @@ impl DataStore {
self: &Arc<Self>, self: &Arc<Self>,
ns: &BackupNamespace, ns: &BackupNamespace,
delete_groups: bool, delete_groups: bool,
) -> Result<bool, Error> { ) -> Result<(bool, BackupGroupDeleteStats), Error> {
let store = self.name(); let store = self.name();
let mut removed_all_requested = true; let mut removed_all_requested = true;
let mut stats = BackupGroupDeleteStats::default();
if delete_groups { if delete_groups {
log::info!("removing whole namespace recursively below {store}:/{ns}",); log::info!("removing whole namespace recursively below {store}:/{ns}",);
for ns in self.recursive_iter_backup_ns(ns.to_owned())? { for ns in self.recursive_iter_backup_ns(ns.to_owned())? {
let removed_ns_groups = self.remove_namespace_groups(&ns?)?; let (removed_ns_groups, delete_stats) = self.remove_namespace_groups(&ns?)?;
stats.add(&delete_stats);
removed_all_requested = removed_all_requested && removed_ns_groups; removed_all_requested = removed_all_requested && removed_ns_groups;
} }
} else { } else {
@ -577,7 +585,7 @@ impl DataStore {
} }
} }
Ok(removed_all_requested) Ok((removed_all_requested, stats))
} }
/// Remove a complete backup group including all snapshots. /// Remove a complete backup group including all snapshots.

View File

@ -34,10 +34,10 @@ use pxar::accessor::aio::Accessor;
use pxar::EntryKind; use pxar::EntryKind;
use pbs_api_types::{ use pbs_api_types::{
print_ns_and_snapshot, print_store_and_ns, Authid, BackupContent, BackupNamespace, BackupType, print_ns_and_snapshot, print_store_and_ns, Authid, BackupContent, BackupGroupDeleteStats,
Counts, CryptMode, DataStoreConfig, DataStoreListItem, DataStoreStatus, BackupNamespace, BackupType, Counts, CryptMode, DataStoreConfig, DataStoreListItem,
GarbageCollectionJobStatus, GroupListItem, JobScheduleStatus, KeepOptions, Operation, DataStoreStatus, GarbageCollectionJobStatus, GroupListItem, JobScheduleStatus, KeepOptions,
PruneJobOptions, SnapshotListItem, SnapshotVerifyState, BACKUP_ARCHIVE_NAME_SCHEMA, Operation, PruneJobOptions, SnapshotListItem, SnapshotVerifyState, BACKUP_ARCHIVE_NAME_SCHEMA,
BACKUP_ID_SCHEMA, BACKUP_NAMESPACE_SCHEMA, BACKUP_TIME_SCHEMA, BACKUP_TYPE_SCHEMA, BACKUP_ID_SCHEMA, BACKUP_NAMESPACE_SCHEMA, BACKUP_TIME_SCHEMA, BACKUP_TYPE_SCHEMA,
DATASTORE_SCHEMA, IGNORE_VERIFIED_BACKUPS_SCHEMA, MAX_NAMESPACE_DEPTH, NS_MAX_DEPTH_SCHEMA, DATASTORE_SCHEMA, IGNORE_VERIFIED_BACKUPS_SCHEMA, MAX_NAMESPACE_DEPTH, NS_MAX_DEPTH_SCHEMA,
PRIV_DATASTORE_AUDIT, PRIV_DATASTORE_BACKUP, PRIV_DATASTORE_MODIFY, PRIV_DATASTORE_PRUNE, PRIV_DATASTORE_AUDIT, PRIV_DATASTORE_BACKUP, PRIV_DATASTORE_MODIFY, PRIV_DATASTORE_PRUNE,
@ -267,8 +267,17 @@ pub fn list_groups(
type: pbs_api_types::BackupGroup, type: pbs_api_types::BackupGroup,
flatten: true, flatten: true,
}, },
"error-on-protected": {
type: bool,
optional: true,
default: true,
description: "Return error when group cannot be deleted because of protected snapshots",
}
}, },
}, },
returns: {
type: BackupGroupDeleteStats,
},
access: { access: {
permission: &Permission::Anybody, permission: &Permission::Anybody,
description: "Requires on /datastore/{store}[/{namespace}] either DATASTORE_MODIFY for any \ description: "Requires on /datastore/{store}[/{namespace}] either DATASTORE_MODIFY for any \
@ -279,9 +288,10 @@ pub fn list_groups(
pub async fn delete_group( pub async fn delete_group(
store: String, store: String,
ns: Option<BackupNamespace>, ns: Option<BackupNamespace>,
error_on_protected: bool,
group: pbs_api_types::BackupGroup, group: pbs_api_types::BackupGroup,
rpcenv: &mut dyn RpcEnvironment, rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> { ) -> Result<BackupGroupDeleteStats, Error> {
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
@ -299,10 +309,13 @@ pub async fn delete_group(
let delete_stats = datastore.remove_backup_group(&ns, &group)?; let delete_stats = datastore.remove_backup_group(&ns, &group)?;
if !delete_stats.all_removed() { if !delete_stats.all_removed() {
if error_on_protected {
bail!("group only partially deleted due to protected snapshots"); bail!("group only partially deleted due to protected snapshots");
} else {
warn!("group only partially deleted due to protected snapshots");
} }
}
Ok(Value::Null) Ok(delete_stats)
}) })
.await? .await?
} }

View File

@ -1,13 +1,12 @@
use anyhow::{bail, Error}; use anyhow::{bail, Error};
use serde_json::Value;
use pbs_config::CachedUserInfo; use pbs_config::CachedUserInfo;
use proxmox_router::{http_bail, ApiMethod, Permission, Router, RpcEnvironment}; use proxmox_router::{http_bail, ApiMethod, Permission, Router, RpcEnvironment};
use proxmox_schema::*; use proxmox_schema::*;
use pbs_api_types::{ use pbs_api_types::{
Authid, BackupNamespace, NamespaceListItem, Operation, DATASTORE_SCHEMA, NS_MAX_DEPTH_SCHEMA, Authid, BackupGroupDeleteStats, BackupNamespace, NamespaceListItem, Operation,
PROXMOX_SAFE_ID_FORMAT, DATASTORE_SCHEMA, NS_MAX_DEPTH_SCHEMA, PROXMOX_SAFE_ID_FORMAT,
}; };
use pbs_datastore::DataStore; use pbs_datastore::DataStore;
@ -138,6 +137,12 @@ pub fn list_namespaces(
optional: true, optional: true,
default: false, default: false,
}, },
"error-on-protected": {
type: bool,
optional: true,
default: true,
description: "Return error when namespace cannot be deleted because of protected snapshots",
}
}, },
}, },
access: { access: {
@ -149,24 +154,32 @@ pub fn delete_namespace(
store: String, store: String,
ns: BackupNamespace, ns: BackupNamespace,
delete_groups: bool, delete_groups: bool,
error_on_protected: bool,
_info: &ApiMethod, _info: &ApiMethod,
rpcenv: &mut dyn RpcEnvironment, rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> { ) -> Result<BackupGroupDeleteStats, Error> {
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
check_ns_modification_privs(&store, &ns, &auth_id)?; check_ns_modification_privs(&store, &ns, &auth_id)?;
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?; let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?;
if !datastore.remove_namespace_recursive(&ns, delete_groups)? { let (removed_all, stats) = datastore.remove_namespace_recursive(&ns, delete_groups)?;
if delete_groups { if !removed_all {
bail!("group only partially deleted due to protected snapshots"); let err_msg = if delete_groups {
"group only partially deleted due to protected snapshots"
} else { } else {
bail!("only partially deleted due to existing groups but `delete-groups` not true "); "only partially deleted due to existing groups but `delete-groups` not true"
};
if error_on_protected {
bail!(err_msg);
} else {
log::warn!("{err_msg}");
} }
} }
Ok(Value::Null) Ok(stats)
} }
pub const ROUTER: Router = Router::new() pub const ROUTER: Router = Router::new()

View File

@ -650,10 +650,12 @@ fn check_and_remove_ns(params: &PullParameters, local_ns: &BackupNamespace) -> R
check_ns_modification_privs(params.target.store.name(), local_ns, &params.owner) check_ns_modification_privs(params.target.store.name(), local_ns, &params.owner)
.map_err(|err| format_err!("Removing {local_ns} not allowed - {err}"))?; .map_err(|err| format_err!("Removing {local_ns} not allowed - {err}"))?;
params let (removed_all, _delete_stats) = params
.target .target
.store .store
.remove_namespace_recursive(local_ns, true) .remove_namespace_recursive(local_ns, true)?;
Ok(removed_all)
} }
fn check_and_remove_vanished_ns( fn check_and_remove_vanished_ns(