public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: "Lukas Wagner" <l.wagner@proxmox.com>
To: "Proxmox Datacenter Manager development discussion"
	<pdm-devel@lists.proxmox.com>,
	"Gabriel Goller" <g.goller@proxmox.com>,
	"Shan Shaji" <s.shaji@proxmox.com>
Subject: Re: [pdm-devel] [PATCH datacenter-manager 2/3] fix #7179: cli: admin: expose acme commands
Date: Tue, 27 Jan 2026 10:17:01 +0100	[thread overview]
Message-ID: <DFZ9BKND6TM3.1CNSKUID7KVOJ@proxmox.com> (raw)
In-Reply-To: <20260123172910.244121-3-s.shaji@proxmox.com>

Hi Shan, thanks for the patch!

Some notes inline.

On Fri Jan 23, 2026 at 6:29 PM CET, Shan Shaji wrote:
> Previously, ACME commands were not exposed through the admin CLI.
> Added the necessary functionality to manage ACME settings directly
> via the command line.
>
> Signed-off-by: Shan Shaji <s.shaji@proxmox.com>
> ---
>  cli/admin/Cargo.toml  |   4 +-
>  cli/admin/src/acme.rs | 442 ++++++++++++++++++++++++++++++++++++++++++
>  cli/admin/src/main.rs |   5 +
>  3 files changed, 450 insertions(+), 1 deletion(-)
>  create mode 100644 cli/admin/src/acme.rs
>
> diff --git a/cli/admin/Cargo.toml b/cli/admin/Cargo.toml
> index e566b39..01afc88 100644
> --- a/cli/admin/Cargo.toml
> +++ b/cli/admin/Cargo.toml
> @@ -22,7 +22,9 @@ proxmox-access-control.workspace = true
>  proxmox-rest-server.workspace = true
>  proxmox-sys.workspace = true
>  proxmox-daemon.workspace = true
> -
> +proxmox-acme.workspace = true
> +proxmox-acme-api.workspace = true
> +proxmox-base64.workspace = true
>  pdm-api-types.workspace = true
>  pdm-config.workspace = true
>  pdm-buildcfg.workspace = true
> diff --git a/cli/admin/src/acme.rs b/cli/admin/src/acme.rs
> new file mode 100644
> index 0000000..81c63f9
> --- /dev/null
> +++ b/cli/admin/src/acme.rs
> @@ -0,0 +1,442 @@
> +use std::io::Write;
> +
> +use anyhow::{bail, Error};
> +use serde_json::Value;
> +
> +use proxmox_acme::async_client::AcmeClient;
> +use proxmox_acme_api::{completion::*, AcmeAccountName, DnsPluginCore, KNOWN_ACME_DIRECTORIES};
> +use proxmox_rest_server::wait_for_local_worker;
> +use proxmox_router::{cli::*, ApiHandler, RpcEnvironment};
> +use proxmox_schema::api;
> +use proxmox_sys::fs::file_get_contents;
> +
> +use server::api as dc_api;
> +
> +pub fn acme_mgmt_cli() -> CommandLineInterface {
> +    let cmd_def = CliCommandMap::new()
> +        .insert("account", account_cli())
> +        .insert("plugin", plugin_cli())
> +        .insert("certificate", cert_cli());
> +
> +    cmd_def.into()
> +}
> +
> +#[api(
> +    input: {
> +        properties: {
> +                "output-format": {
> +                    schema: OUTPUT_FORMAT,
> +                    optional: true,
> +                }
> +            }
> +    }
> +)]
> +/// List ACME accounts.
> +fn list_accounts(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
> +    let output_format = get_output_format(&param);
> +    let info = &dc_api::config::acme::API_METHOD_LIST_ACCOUNTS;
> +    let mut data = match info.handler {
> +        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
> +        _ => unreachable!(),
> +    };
> +
> +    let options = default_table_format_options();
> +    format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
> +
> +    Ok(())
> +}
> +
> +#[api(
> +    input: {
> +        properties: {
> +            name: { type:  AcmeAccountName },
> +            "output-format": {
> +                schema: OUTPUT_FORMAT,
> +                optional: true,
> +            },
> +        }
> +    }
> +)]
> +/// Show ACME account information.
> +async fn get_account(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
> +    let output_format = get_output_format(&param);
> +
> +    let info = &dc_api::config::acme::API_METHOD_GET_ACCOUNT;
> +    let mut data = match info.handler {
> +        ApiHandler::Async(handler) => (handler)(param, info, rpcenv).await?,
> +        _ => unreachable!(),
> +    };
> +
> +    let options = default_table_format_options()
> +        .column(
> +            ColumnConfig::new("account")
> +                .renderer(|value, _record| Ok(serde_json::to_string_pretty(value)?)),
> +        )
> +        .column(ColumnConfig::new("directory"))
> +        .column(ColumnConfig::new("location"))
> +        .column(ColumnConfig::new("tos"));
> +    format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
> +
> +    Ok(())
> +}
> +
> +#[api(
> +    input: {
> +        properties: {
> +            name: { type: AcmeAccountName },
> +            contact: {
> +                description: "List of email addresses.",
> +            },
> +            directory: {
> +                type: String,
> +                description: "The ACME Directory.",
> +                optional: true, },

Formatting is a bit off here :)

> +        }
> +    }
> +)]
> +///Register an ACME account.
> +async fn register_account(
> +    name: AcmeAccountName,
> +    contact: String,
> +    directory: Option<String>,
> +) -> Result<(), Error> {
> +    let (directory_url, custom_directory) = match directory {
> +        Some(directory) => (directory, true),
> +        None => {
> +            println!("Directory endpoints:");
> +            for (i, dir) in KNOWN_ACME_DIRECTORIES.iter().enumerate() {
> +                println!("{}) {}", i, dir.url);
> +            }
> +
> +            println!("{}) Custom", KNOWN_ACME_DIRECTORIES.len());
> +            let mut attempt = 0;
> +            loop {
> +                print!("Enter selection: ");
> +                std::io::stdout().flush()?;
> +
> +                let mut input = String::new();
> +                std::io::stdin().read_line(&mut input)?;

I know most of this function was copied from PBS, but maybe it would
make sense to have a 

fn read_input(prompt: &str) -> Result<String, Error>

and use it for all those prompts here?

I think this could make this function much more readable.


> +
> +                match input.trim().parse::<usize>() {
> +                    Ok(n) if n < KNOWN_ACME_DIRECTORIES.len() => {
> +                        break (KNOWN_ACME_DIRECTORIES[n].url.to_string(), false);
> +                    }
> +                    Ok(n) if n == KNOWN_ACME_DIRECTORIES.len() => {
> +                        input.clear();
> +                        print!("Enter custom directory URI: ");
> +                        std::io::stdout().flush()?;
> +                        std::io::stdin().read_line(&mut input)?;
> +                        break (input.trim().to_owned(), true);
> +                    }
> +                    _ => eprintln!("Invalid selection."),
> +                }
> +
> +                attempt += 1;
> +                if attempt >= 3 {
> +                    bail!("Aborting.");
> +                }
> +            }
> +        }
> +    };
> +
> +    println!("Attempting to fetch Terms of Service from {directory_url:?}");
> +    let mut client = AcmeClient::new(directory_url.clone());
> +    let directory = client.directory().await?;
> +    let tos_agreed = if let Some(tos_url) = directory.terms_of_service_url() {
> +        println!("Terms of Service: {tos_url}");
> +        print!("Do you agree to the above terms? [y|N]: ");
> +        std::io::stdout().flush()?;
> +        let mut input = String::new();
> +        std::io::stdin().read_line(&mut input)?;
> +        input.trim().eq_ignore_ascii_case("y")
> +    } else {
> +        println!("No Terms of Service found, proceeding.");
> +        true
> +    };
> +
> +    let mut eab_enabled = directory.external_account_binding_required();
> +    if !eab_enabled && custom_directory {
> +        print!("Do you want to use external account binding? [y|N]: ");
> +        std::io::stdout().flush()?;
> +        let mut input = String::new();
> +        std::io::stdin().read_line(&mut input)?;
> +        eab_enabled = input.trim().eq_ignore_ascii_case("y");
> +    } else if eab_enabled {
> +        println!("The CA requires external account binding.");
> +    }
> +
> +    let eab_creds = if eab_enabled {
> +        println!("You should have received a key id and a key from your CA.");
> +
> +        print!("Enter EAB key id: ");
> +        std::io::stdout().flush()?;
> +        let mut eab_kid = String::new();
> +        std::io::stdin().read_line(&mut eab_kid)?;
> +
> +        print!("Enter EAB key: ");
> +        std::io::stdout().flush()?;
> +        let mut eab_hmac_key = String::new();
> +        std::io::stdin().read_line(&mut eab_hmac_key)?;
> +
> +        Some((eab_kid.trim().to_owned(), eab_hmac_key.trim().to_owned()))
> +    } else {
> +        None
> +    };
> +
> +    let tos_url = tos_agreed
> +        .then(|| directory.terms_of_service_url().map(str::to_owned))
> +        .flatten();
> +
> +    println!("Registering ACME account '{}'...", &name,);

No need to create a reference here. Also, name could be inlined into the
format string. Also, there is a trailing comma here, which is a bit odd.


> +    let location =
> +        proxmox_acme_api::register_account(&name, contact, tos_url, Some(directory_url), eab_creds)
> +            .await?;

I wonder if it would make sense to actually call the API handler here,
same as for the other commands? This would entail running registration
in a worker task, which would mean that it is visible in the task
history together with a task log. But maybe I'm overlooking something here,
I saw that PBS does this in the same way as here, and maybe there is a
good reason why the registration is not done in a worker.

> +    println!("Registration successful, account URL: {}", location);
> +
> +    Ok(())
> +}
> +

[...]

> diff --git a/cli/admin/src/main.rs b/cli/admin/src/main.rs
> index bcaa0a6..15114c9 100644
> --- a/cli/admin/src/main.rs
> +++ b/cli/admin/src/main.rs
> @@ -9,6 +9,7 @@ use proxmox_router::RpcEnvironment;
>  use proxmox_schema::api;
>  use proxmox_sys::fs::CreateOptions;
>  
> +mod acme;
>  mod remotes;
>  mod support_status;
>  
> @@ -21,13 +22,17 @@ async fn run() -> Result<(), Error> {
>          &pdm_api_types::AccessControlConfig,
>          pdm_buildcfg::configdir!("/access"),
>      )?;
> +    proxmox_acme_api::init(pdm_buildcfg::configdir!("/acme"), false)?;
> +
>      proxmox_log::Logger::from_env("PDM_LOG", proxmox_log::LevelFilter::INFO)
> +        .stderr_on_no_workertask()
>          .stderr()
>          .init()?;

Doing this actually prints all messages logged outside a workertask
*twice*, since this adds two subscribers that will print to stdout.

I think it would make more sense to:

    proxmox_log::Logger::from_env("PDM_LOG", proxmox_log::LevelFilter::INFO)
        .tasklog_pbs()
        .stderr()
        .init()?;

... which should print *all* messages to stderr and messages from within
a task log will also be stored in the task log.

Maybe also ask Gabriel about these, he did a lot of work on logging.

@Gabriel, would my proposal make sense here?

>  
>      server::context::init()?;
>  
>      let cmd_def = CliCommandMap::new()
> +        .insert("acme", acme::acme_mgmt_cli())
>          .insert("remote", remotes::cli())
>          .insert(
>              "report",



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


  reply	other threads:[~2026-01-27  9:16 UTC|newest]

Thread overview: 14+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-01-23 17:29 [pdm-devel] [PATCH datacenter-manager 0/3] fix #7179: expose ACME commands inside admin CLI Shan Shaji
2026-01-23 17:29 ` [pdm-devel] [PATCH datacenter-manager 1/3] cli: admin: make cli handling async Shan Shaji
2026-01-27  9:17   ` Lukas Wagner
2026-01-27 11:27     ` Shan Shaji
2026-01-23 17:29 ` [pdm-devel] [PATCH datacenter-manager 2/3] fix #7179: cli: admin: expose acme commands Shan Shaji
2026-01-27  9:17   ` Lukas Wagner [this message]
2026-01-27 11:14     ` Shan Shaji
2026-01-27 12:09       ` Lukas Wagner
2026-01-27 17:33         ` Shan Shaji
2026-01-28  8:19           ` Lukas Wagner
2026-01-28  9:26     ` Gabriel Goller
2026-01-28  9:35       ` Shan Shaji
2026-02-03 17:55         ` Shan Shaji
2026-01-23 17:29 ` [pdm-devel] [PATCH datacenter-manager 3/3] chore: update proxmox-acme to version 1 Shan Shaji

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=DFZ9BKND6TM3.1CNSKUID7KVOJ@proxmox.com \
    --to=l.wagner@proxmox.com \
    --cc=g.goller@proxmox.com \
    --cc=pdm-devel@lists.proxmox.com \
    --cc=s.shaji@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