diff --git a/proxmox-backup-client/src/main.rs b/proxmox-backup-client/src/main.rs index 179a03d3..a186da60 100644 --- a/proxmox-backup-client/src/main.rs +++ b/proxmox-backup-client/src/main.rs @@ -71,6 +71,7 @@ pub use catalog::*; mod snapshot; pub use snapshot::*; pub mod key; +pub mod namespace; fn record_repository(repo: &BackupRepository) { let base = match BaseDirectories::with_prefix("proxmox-backup") { @@ -1685,6 +1686,7 @@ fn main() { .insert("version", version_cmd_def) .insert("benchmark", benchmark_cmd_def) .insert("change-owner", change_owner_cmd_def) + .insert("namespace", namespace::cli_map()) .alias(&["files"], &["snapshot", "files"]) .alias(&["forget"], &["snapshot", "forget"]) .alias(&["upload-log"], &["snapshot", "upload-log"]) diff --git a/proxmox-backup-client/src/namespace.rs b/proxmox-backup-client/src/namespace.rs new file mode 100644 index 00000000..aa34b5c8 --- /dev/null +++ b/proxmox-backup-client/src/namespace.rs @@ -0,0 +1,170 @@ +use anyhow::{bail, Error}; +use serde_json::{json, Value}; + +use pbs_api_types::BackupNamespace; +use pbs_client::tools::REPO_URL_SCHEMA; + +use proxmox_router::cli::{ + format_and_print_result, get_output_format, CliCommand, CliCommandMap, OUTPUT_FORMAT, +}; +use proxmox_schema::api; + +use crate::{connect, extract_repository_from_value, optional_ns_param, record_repository}; + +#[api( + input: { + properties: { + repository: { + schema: REPO_URL_SCHEMA, + optional: true, + }, + ns: { + type: BackupNamespace, + optional: true, + }, + "max-depth": { + description: "maximum recursion depth", + optional: true, + }, + "output-format": { + schema: OUTPUT_FORMAT, + optional: true, + }, + } + }, +)] +/// List namespaces in a repository. +async fn list_namespaces(param: Value, max_depth: Option) -> Result<(), Error> { + let output_format = get_output_format(¶m); + let repo = extract_repository_from_value(¶m)?; + let backup_ns = optional_ns_param(¶m)?; + + let path = format!("api2/json/admin/datastore/{}/namespace", repo.store()); + + let mut param = json!({}); + + if let Some(max_depth) = max_depth { + param["max-depth"] = max_depth.into(); + } + + if !backup_ns.is_root() { + param["parent"] = serde_json::to_value(backup_ns)?; + } + + let client = connect(&repo)?; + + let mut result = client.get(&path, Some(param)).await?; + + record_repository(&repo); + + if output_format == "text" { + let data: Vec = + serde_json::from_value(result["data"].take())?; + for entry in data { + if entry.ns.is_root() { + continue; + } + + if let Some(comment) = entry.comment { + println!("{} ({comment})", entry.ns); + } else { + println!("{}", entry.ns); + } + } + } else { + format_and_print_result(&result, &output_format); + } + + Ok(()) +} + +#[api( + input: { + properties: { + repository: { + schema: REPO_URL_SCHEMA, + optional: true, + }, + ns: { + type: BackupNamespace, + optional: true, + }, + } + }, +)] +/// Create a new namespace. +async fn create_namespace(param: Value) -> Result<(), Error> { + let repo = extract_repository_from_value(¶m)?; + let mut backup_ns = optional_ns_param(¶m)?; + + let path = format!("api2/json/admin/datastore/{}/namespace", repo.store()); + + let name = match backup_ns.pop() { + Some(name) => name, + None => bail!("root namespace is always present"), + }; + + let param = json!({ + "parent": backup_ns, + "name": name, + }); + + let client = connect(&repo)?; + + let _result = client.post(&path, Some(param)).await?; + + record_repository(&repo); + + Ok(()) +} + +#[api( + input: { + properties: { + repository: { + schema: REPO_URL_SCHEMA, + optional: true, + }, + ns: { + type: BackupNamespace, + optional: true, + }, + } + }, +)] +/// Delete an existing namespace. +async fn delete_namespace(param: Value) -> Result<(), Error> { + let repo = extract_repository_from_value(¶m)?; + let backup_ns = optional_ns_param(¶m)?; + + if backup_ns.is_root() { + bail!("root namespace cannot be deleted"); + } + + let path = format!("api2/json/admin/datastore/{}/namespace", repo.store()); + let param = json!({ "ns": backup_ns }); + + let client = connect(&repo)?; + + let _result = client.delete(&path, Some(param)).await?; + + record_repository(&repo); + + Ok(()) +} + +pub fn cli_map() -> CliCommandMap { + CliCommandMap::new() + .insert( + "list", + CliCommand::new(&API_METHOD_LIST_NAMESPACES).arg_param(&["ns"]), + ) + .insert( + "create", + CliCommand::new(&API_METHOD_CREATE_NAMESPACE).arg_param(&["ns"]), + ) + .insert( + "delete", + CliCommand::new(&API_METHOD_DELETE_NAMESPACE).arg_param(&["ns"]), + ) +}