diff --git a/proxmox-backup-client/src/group.rs b/proxmox-backup-client/src/group.rs new file mode 100644 index 000000000..67f26e261 --- /dev/null +++ b/proxmox-backup-client/src/group.rs @@ -0,0 +1,88 @@ +use anyhow::{bail, Error}; +use serde_json::Value; + +use proxmox_router::cli::{CliCommand, CliCommandMap, Confirmation}; +use proxmox_schema::api; + +use crate::{ + complete_backup_group, complete_namespace, complete_repository, merge_group_into, + REPO_URL_SCHEMA, +}; +use pbs_api_types::{BackupGroup, BackupNamespace}; +use pbs_client::tools::{connect, remove_repository_from_value}; + +pub fn group_mgmt_cli() -> CliCommandMap { + CliCommandMap::new().insert( + "forget", + CliCommand::new(&API_METHOD_FORGET_GROUP) + .arg_param(&["group"]) + .completion_cb("ns", complete_namespace) + .completion_cb("repository", complete_repository) + .completion_cb("group", complete_backup_group), + ) +} + +#[api( + input: { + properties: { + group: { + type: String, + description: "Backup group", + }, + repository: { + schema: REPO_URL_SCHEMA, + optional: true, + }, + ns: { + type: BackupNamespace, + optional: true, + }, + } + } +)] +/// Forget (remove) backup snapshots. +async fn forget_group(group: String, mut param: Value) -> Result<(), Error> { + let backup_group: BackupGroup = group.parse()?; + let repo = remove_repository_from_value(&mut param)?; + let client = connect(&repo)?; + + let mut api_param = param; + merge_group_into(api_param.as_object_mut().unwrap(), backup_group.clone()); + + let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store()); + let result = client.get(&path, Some(api_param.clone())).await?; + let snapshots = result["data"].as_array().unwrap().len(); + + let confirmation = Confirmation::query_with_default( + format!( + "Delete group \"{}\" with {} snapshot(s)?", + backup_group, snapshots + ) + .as_str(), + Confirmation::No, + )?; + if confirmation.is_yes() { + let path = format!("api2/json/admin/datastore/{}/groups", repo.store()); + if let Err(err) = client.delete(&path, Some(api_param)).await { + // "ENOENT: No such file or directory" is part of the error returned when the group + // has not been found. The full error contains the full datastore path and we would + // like to avoid printing that to the console. Checking if it exists before deleting + // the group doesn't work because we currently do not differentiate between an empty + // and a nonexistent group. This would make it impossible to remove empty groups. + if err + .root_cause() + .to_string() + .contains("ENOENT: No such file or directory") + { + bail!("Unable to find backup group!"); + } else { + bail!(err); + } + } + println!("Successfully deleted group!"); + } else { + println!("Abort."); + } + + Ok(()) +} diff --git a/proxmox-backup-client/src/main.rs b/proxmox-backup-client/src/main.rs index f1c7fbf93..5f694b11e 100644 --- a/proxmox-backup-client/src/main.rs +++ b/proxmox-backup-client/src/main.rs @@ -63,21 +63,30 @@ use pbs_key_config::{decrypt_key, rsa_encrypt_key_config, KeyConfig}; use pbs_tools::crypt_config::CryptConfig; use pbs_tools::json; -mod benchmark; -pub use benchmark::*; -mod mount; -pub use mount::*; -mod task; -pub use task::*; -mod catalog; -pub use catalog::*; -mod snapshot; -pub use snapshot::*; -mod helper; -pub(crate) use helper::*; pub mod key; pub mod namespace; +mod benchmark; +pub use benchmark::*; + +mod catalog; +pub use catalog::*; + +mod group; +pub use group::*; + +mod helper; +pub(crate) use helper::*; + +mod mount; +pub use mount::*; + +mod snapshot; +pub use snapshot::*; + +mod task; +pub use task::*; + fn record_repository(repo: &BackupRepository) { let base = match BaseDirectories::with_prefix("proxmox-backup") { Ok(v) => v, @@ -2017,6 +2026,7 @@ fn main() { .insert("benchmark", benchmark_cmd_def) .insert("change-owner", change_owner_cmd_def) .insert("namespace", namespace::cli_map()) + .insert("group", group_mgmt_cli()) .alias(&["files"], &["snapshot", "files"]) .alias(&["forget"], &["snapshot", "forget"]) .alias(&["upload-log"], &["snapshot", "upload-log"])