public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH] client: support individual repository component parameters
@ 2026-03-23 21:11 Thomas Lamprecht
  2026-03-24 13:42 ` Fabian Grünbichler
  2026-03-24 15:58 ` Maximiliano Sandoval
  0 siblings, 2 replies; 4+ messages in thread
From: Thomas Lamprecht @ 2026-03-23 21:11 UTC (permalink / raw)
  To: pbs-devel

The compact repository URL format ([[auth-id@]server[:port]:]datastore)
can be cumbersome to remember the exact syntax and work with.
It also makes it awkward to change a single aspect of the connection,
like switching to a different datastore, without rewriting the whole
string.

So add the "expanded" --server, --port, --datastore, and --auth-id as
separate CLI parameters, mutually exclusive with --repository. Both
forms resolve to the same BackupRepository internally.

Add a BackupRepositoryArgs that holds both the atoms and the combined
URL and replace the existing "repository" parameter in all client
tools, i.e. proxmox-backup-client, proxmox-file-restore, and
proxmox-backup-debug. This struct gets flattened into the cli (api)
schemas such that it's fully backward compatible.

Also add recognition for corresponding environment variables
(PBS_SERVER, PBS_PORT, PBS_DATASTORE, PBS_AUTH_ID) serve as fallback
when neither --repository/PBS_REPOSITORY nor CLI component options are
given.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
---

Disclaimer: only lightly tested! but as this is somewhat of a pain point
I run myself into when adding a new Debian host to backup to a PBS via
the client, I finally wanted to have this improved and polished the POC
I have lying around for some time now already.

 docs/backup-client.rst                 |  60 ++++++++++
 pbs-client/src/backup_repo.rs          | 117 +++++++++++++++++-
 pbs-client/src/tools/mod.rs            | 159 +++++++++++++++++++------
 proxmox-backup-client/src/benchmark.rs |   8 +-
 proxmox-backup-client/src/catalog.rs   |  16 +--
 proxmox-backup-client/src/group.rs     |   8 +-
 proxmox-backup-client/src/main.rs      |  68 +++++------
 proxmox-backup-client/src/mount.rs     |  13 +-
 proxmox-backup-client/src/namespace.rs |  20 ++--
 proxmox-backup-client/src/snapshot.rs  |  52 ++++----
 proxmox-backup-client/src/task.rs      |  20 ++--
 proxmox-file-restore/src/main.rs       |  15 ++-
 src/bin/proxmox_backup_debug/diff.rs   |   9 +-
 13 files changed, 418 insertions(+), 147 deletions(-)

diff --git a/docs/backup-client.rst b/docs/backup-client.rst
index 40962f0e2..4d0467eb1 100644
--- a/docs/backup-client.rst
+++ b/docs/backup-client.rst
@@ -28,6 +28,50 @@ brackets (for example, `[fe80::01]`).
 You can pass the repository with the ``--repository`` command-line option, or
 by setting the ``PBS_REPOSITORY`` environment variable.
 
+Alternatively, you can specify the repository components as separate
+command-line options:
+
+``--server <host>``
+  Backup server address (hostname or IP address). Defaults to ``localhost``.
+
+``--port <number>``
+  Backup server port. Defaults to ``8007``.
+
+``--datastore <name>``
+  Name of the target datastore. Required when using component options instead
+  of ``--repository``.
+
+``--auth-id <user@realm[!token]>``
+  Authentication identity, either a user (``user@realm``) or an API token
+  (``user@realm!tokenname``). Defaults to ``root@pam``.
+
+These options are mutually exclusive with ``--repository``. Both forms
+resolve to the same internal representation, so cached login tickets and
+other session state are shared between them. For example, logging in with
+``--repository`` and then running a backup with ``--server``/``--datastore``
+(or vice versa) reuses the same ticket, as long as the server address and
+user match.
+
+The component options make it easy to change individual parts of the
+connection, for example switching to a different datastore or server without
+having to rewrite the entire repository string. They also simplify usage
+with API tokens, which require escaping the ``@`` separator in the compact
+form:
+
+.. code-block:: console
+
+  # Using component options
+  # proxmox-backup-client backup root.pxar:/ \
+      --auth-id 'user@pbs!backup' --server pbs.example.com --datastore store1
+
+  # Equivalent compact form (the \@ disambiguates the user@realm
+  # separator from the user-to-host separator in the URL format)
+  # proxmox-backup-client backup root.pxar:/ \
+      --repository 'user\@pbs!backup@pbs.example.com:store1'
+
+.. Note:: Remember to quote API token identifiers on the shell, since the
+   exclamation mark (``!``) is a special character in most shells.
+
 The web interface provides copyable repository text in the datastore summary
 with the `Show Connection Information` button.
 
@@ -70,6 +114,22 @@ Environment Variables
 ``PBS_REPOSITORY``
   The default backup repository.
 
+``PBS_SERVER``
+  Default backup server address. Used as a fallback when neither
+  ``--repository`` / ``PBS_REPOSITORY`` nor ``--server`` is given.
+  Requires ``PBS_DATASTORE`` to be set as well.
+
+``PBS_PORT``
+  Default backup server port. Defaults to ``8007`` if unset.
+
+``PBS_DATASTORE``
+  Default datastore name. Used as a fallback when neither ``--repository`` /
+  ``PBS_REPOSITORY`` nor ``--datastore`` is given.
+
+``PBS_AUTH_ID``
+  Default authentication identity (``user@realm`` or
+  ``user@realm!tokenname``). Defaults to ``root@pam`` if unset.
+
 ``PBS_PASSWORD``
   When set, this value is used as the password for the backup server.
   You can also set this to an API token secret.
diff --git a/pbs-client/src/backup_repo.rs b/pbs-client/src/backup_repo.rs
index 458f89dc2..8d2d356f4 100644
--- a/pbs-client/src/backup_repo.rs
+++ b/pbs-client/src/backup_repo.rs
@@ -1,8 +1,121 @@
 use std::fmt;
 
-use anyhow::{format_err, Error};
+use anyhow::{bail, format_err, Error};
+use serde::{Deserialize, Serialize};
 
-use pbs_api_types::{Authid, Userid, BACKUP_REPO_URL_REGEX, IP_V6_REGEX};
+use proxmox_schema::api;
+use proxmox_schema::*;
+
+use pbs_api_types::{
+    Authid, Userid, BACKUP_REPO_URL, BACKUP_REPO_URL_REGEX, DATASTORE_SCHEMA, IP_V6_REGEX,
+};
+
+pub const REPO_URL_SCHEMA: Schema =
+    StringSchema::new("Repository URL: [[auth-id@]server[:port]:]datastore")
+        .format(&BACKUP_REPO_URL)
+        .max_length(256)
+        .schema();
+
+pub const BACKUP_REPO_SERVER_SCHEMA: Schema =
+    StringSchema::new("Backup server address (hostname or IP). Default: localhost")
+        .format(&api_types::DNS_NAME_OR_IP_FORMAT)
+        .max_length(256)
+        .schema();
+
+pub const BACKUP_REPO_PORT_SCHEMA: Schema = IntegerSchema::new("Backup server port. Default: 8007")
+    .minimum(1)
+    .maximum(65535)
+    .default(8007)
+    .schema();
+
+#[api(
+    properties: {
+        repository: {
+            schema: REPO_URL_SCHEMA,
+            optional: true,
+        },
+        server: {
+            schema: BACKUP_REPO_SERVER_SCHEMA,
+            optional: true,
+        },
+        port: {
+            schema: BACKUP_REPO_PORT_SCHEMA,
+            optional: true,
+        },
+        datastore: {
+            schema: DATASTORE_SCHEMA,
+            optional: true,
+        },
+        "auth-id": {
+            type: Authid,
+            optional: true,
+        },
+    },
+)]
+#[derive(Default, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+/// Backup repository location, specified either as a repository URL or as individual components
+/// (server, port, datastore, auth-id).
+pub struct BackupRepositoryArgs {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub repository: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub server: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub port: Option<u16>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub datastore: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub auth_id: Option<Authid>,
+}
+
+impl BackupRepositoryArgs {
+    /// Returns `true` if any atom parameter (server, port, datastore, or auth-id) is set.
+    pub fn has_atoms(&self) -> bool {
+        self.server.is_some()
+            || self.port.is_some()
+            || self.datastore.is_some()
+            || self.auth_id.is_some()
+    }
+}
+
+impl TryFrom<BackupRepositoryArgs> for BackupRepository {
+    type Error = anyhow::Error;
+
+    /// Convert explicit CLI arguments into a [`BackupRepository`].
+    ///
+    /// * If `repository` and any atom are both set, returns an error.
+    /// * If atoms are present, builds the repository from them (requires `datastore`).
+    /// * If only `repository` is set, parses the repo URL.
+    /// * If nothing is set, returns an error - callers must fall back to environment variables /
+    ///   credentials themselves.
+    fn try_from(args: BackupRepositoryArgs) -> Result<Self, Self::Error> {
+        let has_url = args.repository.is_some();
+        let has_atoms = args.has_atoms();
+
+        if has_url && has_atoms {
+            bail!("--repository and --server/--port/--datastore/--auth-id are mutually exclusive");
+        }
+
+        if has_atoms {
+            let store = args.datastore.ok_or_else(|| {
+                format_err!("--datastore is required when not using --repository")
+            })?;
+            return Ok(BackupRepository::new(
+                args.auth_id,
+                args.server,
+                args.port,
+                store,
+            ));
+        }
+
+        if let Some(url) = args.repository {
+            return url.parse();
+        }
+
+        anyhow::bail!("no repository specified")
+    }
+}
 
 /// Reference remote backup locations
 ///
diff --git a/pbs-client/src/tools/mod.rs b/pbs-client/src/tools/mod.rs
index 7a496d14c..ad83d8cab 100644
--- a/pbs-client/src/tools/mod.rs
+++ b/pbs-client/src/tools/mod.rs
@@ -17,12 +17,14 @@ use proxmox_router::cli::{complete_file_name, shellword_split};
 use proxmox_schema::*;
 use proxmox_sys::fs::file_get_json;
 
-use pbs_api_types::{
-    Authid, BackupArchiveName, BackupNamespace, RateLimitConfig, UserWithTokens, BACKUP_REPO_URL,
-};
+use pbs_api_types::{Authid, BackupArchiveName, BackupNamespace, RateLimitConfig, UserWithTokens};
 use pbs_datastore::BackupManifest;
 
-use crate::{BackupRepository, HttpClient, HttpClientOptions};
+use crate::{BackupRepository, BackupRepositoryArgs, HttpClient, HttpClientOptions};
+
+// Re-export for backward compatibility; the canonical definition is now in
+// backup_repo.rs alongside BackupRepositoryArgs.
+pub use crate::REPO_URL_SCHEMA;
 
 pub mod key_source;
 
@@ -30,6 +32,10 @@ const ENV_VAR_PBS_FINGERPRINT: &str = "PBS_FINGERPRINT";
 const ENV_VAR_PBS_PASSWORD: &str = "PBS_PASSWORD";
 const ENV_VAR_PBS_ENCRYPTION_PASSWORD: &str = "PBS_ENCRYPTION_PASSWORD";
 const ENV_VAR_PBS_REPOSITORY: &str = "PBS_REPOSITORY";
+const ENV_VAR_PBS_SERVER: &str = "PBS_SERVER";
+const ENV_VAR_PBS_PORT: &str = "PBS_PORT";
+const ENV_VAR_PBS_DATASTORE: &str = "PBS_DATASTORE";
+const ENV_VAR_PBS_AUTH_ID: &str = "PBS_AUTH_ID";
 
 /// Directory with system [credential]s. See systemd-creds(1).
 ///
@@ -44,11 +50,6 @@ const CRED_PBS_REPOSITORY: &str = "proxmox-backup-client.repository";
 /// Credential name of the the fingerprint.
 const CRED_PBS_FINGERPRINT: &str = "proxmox-backup-client.fingerprint";
 
-pub const REPO_URL_SCHEMA: Schema = StringSchema::new("Repository URL.")
-    .format(&BACKUP_REPO_URL)
-    .max_length(256)
-    .schema();
-
 pub const CHUNK_SIZE_SCHEMA: Schema = IntegerSchema::new("Chunk size in KB. Must be a power of 2.")
     .minimum(64)
     .maximum(4096)
@@ -233,41 +234,131 @@ pub fn get_fingerprint() -> Option<String> {
         .unwrap_or_default()
 }
 
+/// Build [`BackupRepositoryArgs`] from the fields in a JSON Value.
+fn args_from_value(param: &Value) -> BackupRepositoryArgs {
+    BackupRepositoryArgs {
+        repository: param["repository"].as_str().map(String::from),
+        server: param["server"].as_str().map(String::from),
+        port: param["port"].as_u64().map(|p| p as u16),
+        datastore: param["datastore"].as_str().map(String::from),
+        auth_id: param["auth-id"]
+            .as_str()
+            .and_then(|s| s.parse::<Authid>().ok()),
+    }
+}
+
+/// Build [`BackupRepositoryArgs`] from `PBS_*` environment variables.
+fn args_from_env() -> BackupRepositoryArgs {
+    BackupRepositoryArgs {
+        repository: None,
+        server: std::env::var(ENV_VAR_PBS_SERVER).ok(),
+        port: std::env::var(ENV_VAR_PBS_PORT)
+            .ok()
+            .and_then(|p| p.parse::<u16>().ok()),
+        datastore: std::env::var(ENV_VAR_PBS_DATASTORE).ok(),
+        auth_id: std::env::var(ENV_VAR_PBS_AUTH_ID)
+            .ok()
+            .and_then(|s| s.parse::<Authid>().ok()),
+    }
+}
+
+/// Remove repository-related keys from a JSON Value and return the parsed [`BackupRepository`].
+///
+/// This is used by commands that forward the remaining parameters to the server API after stripping
+/// the repository fields.
 pub fn remove_repository_from_value(param: &mut Value) -> Result<BackupRepository, Error> {
-    if let Some(url) = param
+    let map = param
         .as_object_mut()
-        .ok_or_else(|| format_err!("unable to get repository (parameter is not an object)"))?
-        .remove("repository")
-    {
-        return url
-            .as_str()
-            .ok_or_else(|| format_err!("invalid repository value (must be a string)"))?
-            .parse();
+        .ok_or_else(|| format_err!("unable to get repository (parameter is not an object)"))?;
+
+    let to_string = |v: Value| v.as_str().map(String::from);
+
+    let args = BackupRepositoryArgs {
+        repository: map.remove("repository").and_then(to_string),
+        server: map.remove("server").and_then(to_string),
+        port: map
+            .remove("port")
+            .and_then(|v| v.as_u64())
+            .map(|p| p as u16),
+        datastore: map.remove("datastore").and_then(to_string),
+        auth_id: map
+            .remove("auth-id")
+            .and_then(to_string)
+            .map(|s| s.parse::<Authid>())
+            .transpose()?,
+    };
+
+    match BackupRepository::try_from(args) {
+        Ok(repo) => Ok(repo),
+        Err(_) => {
+            // Fall back to environment.
+            if let Some(url) = get_default_repository() {
+                return url.parse();
+            }
+            let env_args = args_from_env();
+            if env_args.has_atoms() {
+                return BackupRepository::try_from(env_args);
+            }
+            bail!("repository not passed via CLI options and unable to get (default) repository from environment");
+        }
     }
-
-    get_default_repository()
-        .ok_or_else(|| format_err!("unable to get default repository"))?
-        .parse()
 }
 
+/// Extract a [`BackupRepository`] from CLI parameters.
+///
+/// Resolution precedence:
+/// 1. CLI `--repository` + CLI atoms → error (mutually exclusive)
+/// 2. CLI atoms (at least `--datastore`)
+/// 3. CLI `--repository`
+/// 4. `PBS_REPOSITORY` environment variable / systemd credential
+/// 5. `PBS_SERVER`/`PBS_PORT`/`PBS_DATASTORE`/`PBS_AUTH_ID` environment variables
 pub fn extract_repository_from_value(param: &Value) -> Result<BackupRepository, Error> {
-    let repo_url = param["repository"]
-        .as_str()
-        .map(String::from)
-        .or_else(get_default_repository)
-        .ok_or_else(|| format_err!("unable to get (default) repository"))?;
+    let args = args_from_value(param);
 
-    let repo: BackupRepository = repo_url.parse()?;
-
-    Ok(repo)
+    match BackupRepository::try_from(args) {
+        Ok(repo) => Ok(repo),
+        Err(_) => {
+            // Fall back to environment.
+            if let Some(url) = get_default_repository() {
+                return url.parse();
+            }
+            let env_args = args_from_env();
+            if env_args.has_atoms() {
+                return BackupRepository::try_from(env_args);
+            }
+            bail!("unable to get (default) repository");
+        }
+    }
 }
 
+/// Extract a [`BackupRepository`] from a parameter map (used for shell
+/// completion callbacks).
 pub fn extract_repository_from_map(param: &HashMap<String, String>) -> Option<BackupRepository> {
-    param
-        .get("repository")
-        .map(String::from)
-        .or_else(get_default_repository)
-        .and_then(|repo_url| repo_url.parse::<BackupRepository>().ok())
+    let args = BackupRepositoryArgs {
+        repository: param.get("repository").cloned(),
+        server: param.get("server").cloned(),
+        port: param.get("port").and_then(|p| p.parse().ok()),
+        datastore: param.get("datastore").cloned(),
+        auth_id: param.get("auth-id").and_then(|s| s.parse().ok()),
+    };
+
+    if let Ok(repo) = BackupRepository::try_from(args) {
+        return Some(repo);
+    }
+
+    // Fall back to environment: compound URL, then atoms.
+    if let Some(url) = get_default_repository() {
+        if let Ok(repo) = url.parse() {
+            return Some(repo);
+        }
+    }
+
+    let env_args = args_from_env();
+    if env_args.has_atoms() {
+        return BackupRepository::try_from(env_args).ok();
+    }
+
+    None
 }
 
 pub fn connect(repo: &BackupRepository) -> Result<HttpClient, Error> {
diff --git a/proxmox-backup-client/src/benchmark.rs b/proxmox-backup-client/src/benchmark.rs
index ad8c60ed9..c113e2ca2 100644
--- a/proxmox-backup-client/src/benchmark.rs
+++ b/proxmox-backup-client/src/benchmark.rs
@@ -22,7 +22,7 @@ use pbs_key_config::{load_and_decrypt_key, KeyDerivationConfig};
 use pbs_tools::crypt_config::CryptConfig;
 
 use crate::{
-    connect, extract_repository_from_value, record_repository, KEYFILE_SCHEMA, REPO_URL_SCHEMA,
+    connect, extract_repository_from_value, record_repository, BackupRepositoryArgs, KEYFILE_SCHEMA,
 };
 
 #[api()]
@@ -105,9 +105,9 @@ static BENCHMARK_RESULT_2020_TOP: BenchmarkResult = BenchmarkResult {
 #[api(
     input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             keyfile: {
                 schema: KEYFILE_SCHEMA,
diff --git a/proxmox-backup-client/src/catalog.rs b/proxmox-backup-client/src/catalog.rs
index b1b22ff24..50d8e997a 100644
--- a/proxmox-backup-client/src/catalog.rs
+++ b/proxmox-backup-client/src/catalog.rs
@@ -20,16 +20,16 @@ use crate::{
     complete_backup_snapshot, complete_group_or_snapshot, complete_namespace,
     complete_pxar_archive_name, complete_repository, connect, crypto_parameters, decrypt_key,
     dir_or_last_from_group, extract_repository_from_value, format_key_source, optional_ns_param,
-    record_repository, BackupDir, BufferedDynamicReader, CatalogReader, DynamicIndexReader,
-    IndexFile, Shell, KEYFD_SCHEMA, REPO_URL_SCHEMA,
+    record_repository, BackupDir, BackupRepositoryArgs, BufferedDynamicReader, CatalogReader,
+    DynamicIndexReader, IndexFile, Shell, KEYFD_SCHEMA,
 };
 
 #[api(
    input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             ns: {
                 type: BackupNamespace,
@@ -170,9 +170,9 @@ async fn dump_catalog(param: Value) -> Result<Value, Error> {
             "archive-name": {
                 type: BackupArchiveName,
             },
-            "repository": {
-                optional: true,
-                schema: REPO_URL_SCHEMA,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             "keyfile": {
                 optional: true,
diff --git a/proxmox-backup-client/src/group.rs b/proxmox-backup-client/src/group.rs
index 2c12db2ee..5a3786f07 100644
--- a/proxmox-backup-client/src/group.rs
+++ b/proxmox-backup-client/src/group.rs
@@ -6,7 +6,7 @@ use proxmox_schema::api;
 
 use crate::{
     complete_backup_group, complete_namespace, complete_repository, merge_group_into,
-    REPO_URL_SCHEMA,
+    BackupRepositoryArgs,
 };
 use pbs_api_types::{BackupGroup, BackupNamespace};
 use pbs_client::tools::{connect, remove_repository_from_value};
@@ -29,9 +29,9 @@ pub fn group_mgmt_cli() -> CliCommandMap {
                 type: String,
                 description: "Backup group",
             },
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             ns: {
                 type: BackupNamespace,
diff --git a/proxmox-backup-client/src/main.rs b/proxmox-backup-client/src/main.rs
index 2ada87bdd..55fef0cca 100644
--- a/proxmox-backup-client/src/main.rs
+++ b/proxmox-backup-client/src/main.rs
@@ -41,13 +41,13 @@ use pbs_client::tools::{
         crypto_parameters, format_key_source, get_encryption_key_password, KEYFD_SCHEMA,
         KEYFILE_SCHEMA, MASTER_PUBKEY_FD_SCHEMA, MASTER_PUBKEY_FILE_SCHEMA,
     },
-    raise_nofile_limit, CHUNK_SIZE_SCHEMA, REPO_URL_SCHEMA,
+    raise_nofile_limit, CHUNK_SIZE_SCHEMA,
 };
 use pbs_client::{
     delete_ticket_info, parse_backup_specification, view_task_result, BackupDetectionMode,
-    BackupReader, BackupRepository, BackupSpecificationType, BackupStats, BackupWriter,
-    BackupWriterOptions, ChunkStream, FixedChunkStream, HttpClient, IndexType, InjectionData,
-    PxarBackupStream, RemoteChunkReader, UploadOptions, BACKUP_SOURCE_SCHEMA,
+    BackupReader, BackupRepository, BackupRepositoryArgs, BackupSpecificationType, BackupStats,
+    BackupWriter, BackupWriterOptions, ChunkStream, FixedChunkStream, HttpClient, IndexType,
+    InjectionData, PxarBackupStream, RemoteChunkReader, UploadOptions, BACKUP_SOURCE_SCHEMA,
 };
 use pbs_datastore::catalog::{BackupCatalogWriter, CatalogReader, CatalogWriter};
 use pbs_datastore::chunk_store::verify_chunk_size;
@@ -340,9 +340,9 @@ pub fn optional_ns_param(param: &Value) -> Result<BackupNamespace, Error> {
 #[api(
    input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             "ns": {
                 type: BackupNamespace,
@@ -433,9 +433,9 @@ fn merge_group_into(to: &mut serde_json::Map<String, Value>, group: BackupGroup)
 #[api(
    input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             group: {
                 type: String,
@@ -478,9 +478,9 @@ async fn change_backup_owner(group: String, mut param: Value) -> Result<(), Erro
 #[api(
    input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
         }
    }
@@ -500,9 +500,9 @@ async fn api_login(param: Value) -> Result<Value, Error> {
 #[api(
    input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
         }
    }
@@ -519,9 +519,9 @@ fn api_logout(param: Value) -> Result<Value, Error> {
 #[api(
    input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             "output-format": {
                 schema: OUTPUT_FORMAT,
@@ -572,9 +572,9 @@ async fn api_version(param: Value) -> Result<(), Error> {
 #[api(
     input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             "output-format": {
                 schema: OUTPUT_FORMAT,
@@ -662,9 +662,9 @@ fn spawn_catalog_upload(
                     schema: BACKUP_SOURCE_SCHEMA,
                 }
             },
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             "include-dev": {
                 description:
@@ -1439,9 +1439,9 @@ async fn dump_image<W: Write>(
 #[api(
     input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             ns: {
                 type: BackupNamespace,
@@ -1837,9 +1837,9 @@ async fn restore(
                 default: false,
                 description: "Minimal output - only show removals.",
             },
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
         },
     },
@@ -1929,9 +1929,9 @@ async fn prune(
 #[api(
    input: {
        properties: {
-           repository: {
-               schema: REPO_URL_SCHEMA,
-               optional: true,
+           repo: {
+               type: BackupRepositoryArgs,
+               flatten: true,
            },
            "output-format": {
                schema: OUTPUT_FORMAT,
diff --git a/proxmox-backup-client/src/mount.rs b/proxmox-backup-client/src/mount.rs
index e815c8a9c..bc666a18f 100644
--- a/proxmox-backup-client/src/mount.rs
+++ b/proxmox-backup-client/src/mount.rs
@@ -17,11 +17,12 @@ use proxmox_router::{cli::*, ApiHandler, ApiMethod, RpcEnvironment};
 use proxmox_schema::*;
 use proxmox_sortable_macro::sortable;
 
-use pbs_api_types::{ArchiveType, BackupArchiveName, BackupNamespace};
+use pbs_api_types::{ArchiveType, Authid, BackupArchiveName, BackupNamespace, DATASTORE_SCHEMA};
 use pbs_client::tools::key_source::{
     crypto_parameters, format_key_source, get_encryption_key_password,
 };
 use pbs_client::{BackupReader, RemoteChunkReader};
+use pbs_client::{BACKUP_REPO_PORT_SCHEMA, BACKUP_REPO_SERVER_SCHEMA, REPO_URL_SCHEMA};
 use pbs_datastore::cached_chunk_reader::CachedChunkReader;
 use pbs_datastore::index::IndexFile;
 use pbs_key_config::decrypt_key;
@@ -32,7 +33,7 @@ use crate::helper;
 use crate::{
     complete_group_or_snapshot, complete_img_archive_name, complete_namespace,
     complete_pxar_archive_name, complete_repository, connect, dir_or_last_from_group,
-    extract_repository_from_value, optional_ns_param, record_repository, REPO_URL_SCHEMA,
+    extract_repository_from_value, optional_ns_param, record_repository,
 };
 
 #[sortable]
@@ -54,6 +55,10 @@ const API_METHOD_MOUNT: ApiMethod = ApiMethod::new(
                 &StringSchema::new("Target directory path.").schema()
             ),
             ("repository", true, &REPO_URL_SCHEMA),
+            ("server", true, &BACKUP_REPO_SERVER_SCHEMA),
+            ("port", true, &BACKUP_REPO_PORT_SCHEMA),
+            ("datastore", true, &DATASTORE_SCHEMA),
+            ("auth-id", true, &Authid::API_SCHEMA),
             (
                 "keyfile",
                 true,
@@ -85,6 +90,10 @@ WARNING: Only do this with *trusted* backups!",
             ),
             ("archive-name", false, &BackupArchiveName::API_SCHEMA),
             ("repository", true, &REPO_URL_SCHEMA),
+            ("server", true, &BACKUP_REPO_SERVER_SCHEMA),
+            ("port", true, &BACKUP_REPO_PORT_SCHEMA),
+            ("datastore", true, &DATASTORE_SCHEMA),
+            ("auth-id", true, &Authid::API_SCHEMA),
             (
                 "keyfile",
                 true,
diff --git a/proxmox-backup-client/src/namespace.rs b/proxmox-backup-client/src/namespace.rs
index 2929e394b..53d4c31af 100644
--- a/proxmox-backup-client/src/namespace.rs
+++ b/proxmox-backup-client/src/namespace.rs
@@ -2,7 +2,7 @@ use anyhow::{bail, Error};
 use serde_json::{json, Value};
 
 use pbs_api_types::BackupNamespace;
-use pbs_client::tools::REPO_URL_SCHEMA;
+use pbs_client::BackupRepositoryArgs;
 
 use proxmox_router::cli::{
     format_and_print_result, get_output_format, CliCommand, CliCommandMap, OUTPUT_FORMAT,
@@ -17,9 +17,9 @@ use crate::{
 #[api(
     input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             ns: {
                 type: BackupNamespace,
@@ -84,9 +84,9 @@ async fn list_namespaces(param: Value, max_depth: Option<usize>) -> Result<(), E
 #[api(
     input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             ns: {
                 type: BackupNamespace,
@@ -124,9 +124,9 @@ async fn create_namespace(param: Value) -> Result<(), Error> {
 #[api(
     input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             ns: {
                 type: BackupNamespace,
diff --git a/proxmox-backup-client/src/snapshot.rs b/proxmox-backup-client/src/snapshot.rs
index 500fd2bb8..7c7217891 100644
--- a/proxmox-backup-client/src/snapshot.rs
+++ b/proxmox-backup-client/src/snapshot.rs
@@ -17,8 +17,8 @@ use pbs_tools::json::required_string_param;
 use crate::{
     api_datastore_list_snapshots, complete_backup_group, complete_backup_snapshot,
     complete_namespace, complete_repository, connect, crypto_parameters,
-    extract_repository_from_value, optional_ns_param, record_repository, BackupDir, KEYFD_SCHEMA,
-    KEYFILE_SCHEMA, REPO_URL_SCHEMA,
+    extract_repository_from_value, optional_ns_param, record_repository, BackupDir,
+    BackupRepositoryArgs, KEYFD_SCHEMA, KEYFILE_SCHEMA,
 };
 
 fn snapshot_args(ns: &BackupNamespace, snapshot: &BackupDir) -> Result<Value, Error> {
@@ -32,9 +32,9 @@ fn snapshot_args(ns: &BackupNamespace, snapshot: &BackupDir) -> Result<Value, Er
 #[api(
    input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             ns: {
                 type: BackupNamespace,
@@ -108,9 +108,9 @@ async fn list_snapshots(param: Value) -> Result<Value, Error> {
 #[api(
    input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             ns: {
                 type: BackupNamespace,
@@ -161,9 +161,9 @@ async fn list_snapshot_files(param: Value) -> Result<Value, Error> {
 #[api(
     input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             ns: {
                 type: BackupNamespace,
@@ -200,9 +200,9 @@ async fn forget_snapshots(param: Value) -> Result<(), Error> {
 #[api(
     input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             ns: {
                 type: BackupNamespace,
@@ -281,9 +281,9 @@ async fn upload_log(param: Value) -> Result<Value, Error> {
 #[api(
     input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             ns: {
                 type: BackupNamespace,
@@ -338,9 +338,9 @@ async fn show_notes(param: Value) -> Result<Value, Error> {
 #[api(
     input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             ns: {
                 type: BackupNamespace,
@@ -380,9 +380,9 @@ async fn update_notes(param: Value) -> Result<Value, Error> {
 #[api(
     input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             ns: {
                 type: BackupNamespace,
@@ -437,9 +437,9 @@ async fn show_protection(param: Value) -> Result<(), Error> {
 #[api(
     input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             ns: {
                 type: BackupNamespace,
diff --git a/proxmox-backup-client/src/task.rs b/proxmox-backup-client/src/task.rs
index 472db0860..dba50cc85 100644
--- a/proxmox-backup-client/src/task.rs
+++ b/proxmox-backup-client/src/task.rs
@@ -10,14 +10,14 @@ use pbs_tools::json::required_string_param;
 
 use pbs_api_types::UPID;
 
-use crate::{complete_repository, connect, extract_repository_from_value, REPO_URL_SCHEMA};
+use crate::{complete_repository, connect, extract_repository_from_value, BackupRepositoryArgs};
 
 #[api(
     input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             limit: {
                 description: "The maximal number of tasks to list.",
@@ -87,9 +87,9 @@ async fn task_list(param: Value) -> Result<Value, Error> {
 #[api(
     input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             upid: {
                 type: UPID,
@@ -112,9 +112,9 @@ async fn task_log(param: Value) -> Result<Value, Error> {
 #[api(
     input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             upid: {
                 type: UPID,
diff --git a/proxmox-file-restore/src/main.rs b/proxmox-file-restore/src/main.rs
index bf6cf9aed..99685bafc 100644
--- a/proxmox-file-restore/src/main.rs
+++ b/proxmox-file-restore/src/main.rs
@@ -31,9 +31,8 @@ use pbs_client::tools::{
         crypto_parameters_keep_fd, format_key_source, get_encryption_key_password, KEYFD_SCHEMA,
         KEYFILE_SCHEMA,
     },
-    REPO_URL_SCHEMA,
 };
-use pbs_client::{BackupReader, BackupRepository, RemoteChunkReader};
+use pbs_client::{BackupReader, BackupRepository, BackupRepositoryArgs, RemoteChunkReader};
 use pbs_datastore::catalog::{ArchiveEntry, CatalogReader, DirEntryAttribute};
 use pbs_datastore::dynamic_index::BufferedDynamicReader;
 use pbs_datastore::index::IndexFile;
@@ -212,9 +211,9 @@ async fn list_files(
 #[api(
     input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             ns: {
                 type: BackupNamespace,
@@ -361,9 +360,9 @@ async fn list(
 #[api(
     input: {
         properties: {
-            repository: {
-                schema: REPO_URL_SCHEMA,
-                optional: true,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             ns: {
                 type: BackupNamespace,
diff --git a/src/bin/proxmox_backup_debug/diff.rs b/src/bin/proxmox_backup_debug/diff.rs
index 116216e51..f13cc6ab5 100644
--- a/src/bin/proxmox_backup_debug/diff.rs
+++ b/src/bin/proxmox_backup_debug/diff.rs
@@ -19,9 +19,8 @@ use pbs_client::tools::key_source::{
 };
 use pbs_client::tools::{
     complete_archive_name, complete_group_or_snapshot, connect, extract_repository_from_value,
-    REPO_URL_SCHEMA,
 };
-use pbs_client::{BackupReader, BackupRepository, RemoteChunkReader};
+use pbs_client::{BackupReader, BackupRepository, BackupRepositoryArgs, RemoteChunkReader};
 use pbs_datastore::dynamic_index::{BufferedDynamicReader, DynamicIndexReader, LocalDynamicReadAt};
 use pbs_datastore::index::IndexFile;
 use pbs_key_config::decrypt_key;
@@ -72,9 +71,9 @@ pub fn diff_commands() -> CommandLineInterface {
             "archive-name": {
                 type: BackupArchiveName,
             },
-            "repository": {
-                optional: true,
-                schema: REPO_URL_SCHEMA,
+            repo: {
+                type: BackupRepositoryArgs,
+                flatten: true,
             },
             "keyfile": {
                 optional: true,
-- 
2.47.3





^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-03-25 11:27 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-03-23 21:11 [PATCH] client: support individual repository component parameters Thomas Lamprecht
2026-03-24 13:42 ` Fabian Grünbichler
2026-03-25 11:28   ` Thomas Lamprecht
2026-03-24 15:58 ` Maximiliano Sandoval

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal