[pbs-devel] [PATCH proxmox-backup v8 05/45] api/cli: add endpoint and command to check s3 client connection

Lukas Wagner l.wagner at proxmox.com
Fri Jul 18 09:43:01 CEST 2025


With the magic string replaced by constants:

Reviewed-by: Lukas Wagner <l.wagner at proxmox.com>


On  2025-07-15 14:52, Christian Ebner wrote:
> Adds a dedicated api endpoint and a proxmox-backup-manager command to
> check if the configured S3 client can reach the bucket.
> 
> Signed-off-by: Christian Ebner <c.ebner at proxmox.com>
> ---
> changes since version 7:
> - no changes
> 
>  src/api2/admin/mod.rs                 |  2 +
>  src/api2/admin/s3.rs                  | 80 +++++++++++++++++++++++++++
>  src/bin/proxmox-backup-manager.rs     |  1 +
>  src/bin/proxmox_backup_manager/mod.rs |  2 +
>  src/bin/proxmox_backup_manager/s3.rs  | 46 +++++++++++++++
>  5 files changed, 131 insertions(+)
>  create mode 100644 src/api2/admin/s3.rs
>  create mode 100644 src/bin/proxmox_backup_manager/s3.rs
> 
> diff --git a/src/api2/admin/mod.rs b/src/api2/admin/mod.rs
> index a1c49f8e2..7694de4b9 100644
> --- a/src/api2/admin/mod.rs
> +++ b/src/api2/admin/mod.rs
> @@ -9,6 +9,7 @@ pub mod gc;
>  pub mod metrics;
>  pub mod namespace;
>  pub mod prune;
> +pub mod s3;
>  pub mod sync;
>  pub mod traffic_control;
>  pub mod verify;
> @@ -19,6 +20,7 @@ const SUBDIRS: SubdirMap = &sorted!([
>      ("metrics", &metrics::ROUTER),
>      ("prune", &prune::ROUTER),
>      ("gc", &gc::ROUTER),
> +    ("s3", &s3::ROUTER),
>      ("sync", &sync::ROUTER),
>      ("traffic-control", &traffic_control::ROUTER),
>      ("verify", &verify::ROUTER),
> diff --git a/src/api2/admin/s3.rs b/src/api2/admin/s3.rs
> new file mode 100644
> index 000000000..d20031707
> --- /dev/null
> +++ b/src/api2/admin/s3.rs
> @@ -0,0 +1,80 @@
> +//! S3 bucket operations
> +
> +use anyhow::{Context, Error};
> +use serde_json::Value;
> +
> +use proxmox_http::Body;
> +use proxmox_router::{list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap};
> +use proxmox_s3_client::{
> +    S3Client, S3ClientConfig, S3ClientOptions, S3ClientSecretsConfig, S3_BUCKET_NAME_SCHEMA,
> +    S3_CLIENT_ID_SCHEMA,
> +};
> +use proxmox_schema::*;
> +use proxmox_sortable_macro::sortable;
> +
> +use pbs_api_types::PRIV_SYS_MODIFY;
> +
> +#[api(
> +    input: {
> +        properties: {
> +            "s3-client-id": {
> +                schema: S3_CLIENT_ID_SCHEMA,
> +            },
> +            bucket: {
> +                schema: S3_BUCKET_NAME_SCHEMA,
> +            },
> +            "store-prefix": {
> +                type: String,
> +                description: "Store prefix within bucket for S3 object keys (commonly datastore name)",
> +            },
> +        },
> +    },
> +    access: {
> +        permission: &Permission::Privilege(&[], PRIV_SYS_MODIFY, false),
> +    },
> +)]
> +/// Perform basic sanity check for given s3 client configuration
> +pub async fn check(
> +    s3_client_id: String,
> +    bucket: String,
> +    store_prefix: String,
> +    _rpcenv: &mut dyn RpcEnvironment,
> +) -> Result<Value, Error> {
> +    let (config, _digest) = pbs_config::s3::config()?;
> +    let config: S3ClientConfig = config
> +        .lookup("s3client", &s3_client_id)
> +        .context("config lookup failed")?;
> +    let (secrets, _secrets_digest) = pbs_config::s3::secrets_config()?;
> +    let secrets: S3ClientSecretsConfig = secrets
> +        .lookup("s3secrets", &s3_client_id)
> +        .context("secrets lookup failed")?;

Same thing here with regards to the section config type strings.

> +
> +    let options = S3ClientOptions::from_config(config, secrets, bucket, store_prefix);
> +
> +    let test_object_key = ".s3-client-test";
> +    let client = S3Client::new(options).context("client creation failed")?;
> +    client.head_bucket().await.context("head object failed")?;
> +    client
> +        .put_object(test_object_key.into(), Body::empty(), true)
> +        .await
> +        .context("put object failed")?;
> +    client
> +        .get_object(test_object_key.into())
> +        .await
> +        .context("get object failed")?;
> +    client
> +        .delete_object(test_object_key.into())
> +        .await
> +        .context("delete object failed")?;
> +
> +    Ok(Value::Null)
> +}
> +
> +#[sortable]
> +const S3_OPERATION_SUBDIRS: SubdirMap = &[("check", &Router::new().get(&API_METHOD_CHECK))];
> +
> +const S3_OPERATION_ROUTER: Router = Router::new()
> +    .get(&list_subdirs_api_method!(S3_OPERATION_SUBDIRS))
> +    .subdirs(S3_OPERATION_SUBDIRS);
> +
> +pub const ROUTER: Router = Router::new().match_all("s3-client-id", &S3_OPERATION_ROUTER);
> diff --git a/src/bin/proxmox-backup-manager.rs b/src/bin/proxmox-backup-manager.rs
> index d4363e717..68d87c676 100644
> --- a/src/bin/proxmox-backup-manager.rs
> +++ b/src/bin/proxmox-backup-manager.rs
> @@ -677,6 +677,7 @@ async fn run() -> Result<(), Error> {
>          .insert("garbage-collection", garbage_collection_commands())
>          .insert("acme", acme_mgmt_cli())
>          .insert("cert", cert_mgmt_cli())
> +        .insert("s3", s3_commands())
>          .insert("subscription", subscription_commands())
>          .insert("sync-job", sync_job_commands())
>          .insert("verify-job", verify_job_commands())
> diff --git a/src/bin/proxmox_backup_manager/mod.rs b/src/bin/proxmox_backup_manager/mod.rs
> index 9b5c73e9a..312a6db6b 100644
> --- a/src/bin/proxmox_backup_manager/mod.rs
> +++ b/src/bin/proxmox_backup_manager/mod.rs
> @@ -26,6 +26,8 @@ mod prune;
>  pub use prune::*;
>  mod remote;
>  pub use remote::*;
> +mod s3;
> +pub use s3::*;
>  mod subscription;
>  pub use subscription::*;
>  mod sync;
> diff --git a/src/bin/proxmox_backup_manager/s3.rs b/src/bin/proxmox_backup_manager/s3.rs
> new file mode 100644
> index 000000000..9bb89ff55
> --- /dev/null
> +++ b/src/bin/proxmox_backup_manager/s3.rs
> @@ -0,0 +1,46 @@
> +use proxmox_router::{cli::*, RpcEnvironment};
> +use proxmox_s3_client::{S3_BUCKET_NAME_SCHEMA, S3_CLIENT_ID_SCHEMA};
> +use proxmox_schema::api;
> +
> +use proxmox_backup::api2;
> +
> +use anyhow::Error;
> +use serde_json::Value;
> +
> +#[api(
> +    input: {
> +        properties: {
> +            "s3-client-id": {
> +                schema: S3_CLIENT_ID_SCHEMA,
> +            },
> +            bucket: {
> +                schema: S3_BUCKET_NAME_SCHEMA,
> +            },
> +            "store-prefix": {
> +                type: String,
> +                description: "Store prefix within bucket for S3 object keys (commonly datastore name)",
> +            },
> +        },
> +    },
> +)]
> +/// Perform basic sanity checks for given S3 client configuration
> +async fn check(
> +    s3_client_id: String,
> +    bucket: String,
> +    store_prefix: String,
> +    rpcenv: &mut dyn RpcEnvironment,
> +) -> Result<Value, Error> {
> +    api2::admin::s3::check(s3_client_id, bucket, store_prefix, rpcenv).await?;
> +    Ok(Value::Null)
> +}
> +
> +pub fn s3_commands() -> CommandLineInterface {
> +    let cmd_def = CliCommandMap::new().insert(
> +        "check",
> +        CliCommand::new(&API_METHOD_CHECK)
> +            .arg_param(&["s3-client-id", "bucket"])
> +            .completion_cb("s3-client-id", pbs_config::s3::complete_s3_client_id),
> +    );
> +
> +    cmd_def.into()
> +}

-- 
- Lukas





More information about the pbs-devel mailing list