public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Thomas Lamprecht <t.lamprecht@proxmox.com>
To: "Fabian Grünbichler" <f.gruenbichler@proxmox.com>,
	pbs-devel@lists.proxmox.com
Subject: Re: [PATCH] client: support individual repository component parameters
Date: Wed, 25 Mar 2026 12:28:08 +0100	[thread overview]
Message-ID: <48da8840-9203-43e5-aa87-3559a04f310a@proxmox.com> (raw)
In-Reply-To: <1774358455.07xtrnyeyt.astroid@yuna.none>

Am 24.03.26 um 14:42 schrieb Fabian Grünbichler:
> On March 23, 2026 10:11 pm, Thomas Lamprecht wrote:
>> +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:
> 
> that last sentence I don't understand, the `@` is part of both a user
> and an API token authid, it's the `!` that is only there for tokens?
> 
> I've never had to escape the `@` in `--repository`..

yeah, that was more for inside perl use cases, but it's bogus; will drop.

[...]

>> @@ -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.
>> +
> 
> we also have a request open for PBS_NAMESPACE, with similar rationale as
> your changes here:
> 
> https://bugzilla.proxmox.com/show_bug.cgi?id=5340

Yeah, that makes sense to incorporate in this series, thanks for the pointer.


>> 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};

[...]

>> +/// 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");
> 
> a similar issue like below also exists here I think, the code path below
> was just how I triggered it..
> 
>> +        }
>>      }
>> -
>> -    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)
> 
> this doesn't work nicely in practice atm, e.g. when I run
> 
> `proxmox-backup-client backup .. --repository "something" --auth-id "something"` :
> 
> Error: unable to get (default) repository
> 
> instead of a nice error point at the invalid combination of arguments..

ack
 
>> +/// 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(_) => {
> 
> probably this error here
> 
>> +            // 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");
> 
> should be printed here? or we should not fall back to the environment if
> at least one atom or the URL is given as argument? what about mix and
> match, e.g. setting the server via the env, but the datastore and
> namespace via arguments?

I checked a few CLI tools and most allow mixing environment and CLI for
different such atoms, e.g. restic or borg, but also others like mysql or
psql, and rethinking this also what I would expect to happen. I'll recheck
this and try to add some unit tests to check and encode the inteded behavior.


>> +/// 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
>>  }
> 
> this helper probably requires similar attention for consistency's sake
> 


Will recheck more closely, thanks for taking a look!





  reply	other threads:[~2026-03-25 11:27 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-03-23 21:11 Thomas Lamprecht
2026-03-24 13:42 ` Fabian Grünbichler
2026-03-25 11:28   ` Thomas Lamprecht [this message]
2026-03-24 15:58 ` Maximiliano Sandoval

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=48da8840-9203-43e5-aa87-3559a04f310a@proxmox.com \
    --to=t.lamprecht@proxmox.com \
    --cc=f.gruenbichler@proxmox.com \
    --cc=pbs-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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