From: "Shan Shaji" <s.shaji@proxmox.com>
To: "Lukas Wagner" <l.wagner@proxmox.com>,
"Proxmox Datacenter Manager development discussion"
<pdm-devel@lists.proxmox.com>,
"Gabriel Goller" <g.goller@proxmox.com>
Subject: Re: [pdm-devel] [PATCH datacenter-manager 2/3] fix #7179: cli: admin: expose acme commands
Date: Tue, 27 Jan 2026 12:14:37 +0100 [thread overview]
Message-ID: <DFZBTLT0VMLY.3MR3NN30JSQ4C@proxmox.com> (raw)
In-Reply-To: <DFZ9BKND6TM3.1CNSKUID7KVOJ@proxmox.com>
Hi Lukas, Thank you for your feedbacks.
Some comments inline.
On Tue Jan 27, 2026 at 10:17 AM CET, Lukas Wagner wrote:
> 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(¶m);
>> + 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(¶m);
>> +
>> + 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 :)
Will fix it in the next one.
>> + }
>> + }
>> +)]
>> +///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.
I will seperate it to another function. Thank you!
>> +
>> + 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.
I also had that smilliar confusion here, anyways i will try using the
API handler here and see if i am getting any issues.
>> + 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.
or could we keep the `stderr_on_no_workertask()` and remove the
`stderr`?
> Maybe also ask Gabriel about these, he did a lot of work on logging.
Will ask him about it. Thank You!
> @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
next prev parent reply other threads:[~2026-01-27 11:14 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
2026-01-27 11:14 ` Shan Shaji [this message]
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=DFZBTLT0VMLY.3MR3NN30JSQ4C@proxmox.com \
--to=s.shaji@proxmox.com \
--cc=g.goller@proxmox.com \
--cc=l.wagner@proxmox.com \
--cc=pdm-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