all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [pdm-devel] [PATCH datacenter-manager 0/3] fix #7179: expose ACME commands inside admin CLI
@ 2026-01-23 17:29 Shan Shaji
  2026-01-23 17:29 ` [pdm-devel] [PATCH datacenter-manager 1/3] cli: admin: make cli handling async Shan Shaji
                   ` (2 more replies)
  0 siblings, 3 replies; 4+ messages in thread
From: Shan Shaji @ 2026-01-23 17:29 UTC (permalink / raw)
  To: pdm-devel

Previously, ACME commands were not exposed through the admin CLI.
Added the necessary functionality to manage ACME settings directly
via the command line. The changes are done by taking reference from 
the proxmox-backup codebase. 

Since i am using the completion handlers from the proxmox-acme-api
crate the following patch [1] needs to be applied before testing or merging
the changes of this patch series. 

**note**: Sending this series only for an initial review as the completions
are not working and also to know if i have missed anything :). 

Testing 
=======

In general i have verified the following commands ie,
- account (deactivate, info, list, update)
- certificate (order, revoke)
- plugin (add, config, list, remove, set)

### Certifcate Creation 

http-01 challenge:
-----------------

I have tested the http-01 challenge verification using a test
pebble server. 
    
Steps followed to test the changes:

1. Installed the changes inside a PDM VM. 
2. install Pebble from Let's Encrypt [2] on the same VM:

    cd
    apt update
    apt install -y golang git
    git clone https://github.com/letsencrypt/pebble
    cd pebble
    go build ./cmd/pebble

then, download and trust the Pebble cert:

    wget https://raw.githubusercontent.com/letsencrypt/pebble/main/test/certs/pebble.minica.pem
    cp pebble.minica.pem /usr/local/share/ca-certificates/pebble.minica.crt
    update-ca-certificates

We want Pebble to perform HTTP-01 validation against port 80, because
PDM's standalone plugin will bind port 80. Set httpPort to 80.

    nano ./test/config/pebble-config.json

Start the Pebble server in the background:

    ./pebble -config ./test/config/pebble-config.json &

Created a Pebble ACME account:

    proxmox-datacenter-manager-admin acme account register default admin@example.com --directory 'https://127.0.0.1:14000/dir'

Added a new ACME domain pdm.proxmox.com with HTTP challenge type. Then
ran the following command.  
   
   proxmox-datacenter-manager admin acme certificate order --force true

Checked if the certificate is validated by the pebble CA. 

Ran the revoke command and verified if the certificate is self-signed
after force refresh. 

---

DNS-01 challenge: 
----------------

I tested the changes with my domain using the cloudflare plugin. 

Steps followed to test the changes:

1. Created an ACME account using let's encrypt staging API. 
2. Add a new plugin using the following command

   proxmox-datacenter-manager-admin acme plugin add dns cloudflare --api cf --data ./cf_tokens 
   cf_tokens had the following credentials:
      - CF_Account_ID=""
	  - CF_Token=""
3. Added my cloudflare managed domain under ACME Domains using the UI. 
4. Ordered the certificate using the following command. 

    proxmox-datacenter-manager-admin acme certificate order --force true

5. Force refreshed the browser and verified that the new certificate is
   verified by (STAGING) Let's Encrypt

6. Revoked the certificate using the following command. 
    
	proxmox-datacenter-manager-admin acme certificate revoke

7. Verified the new certificate is self-signed.

[1] - https://lore.proxmox.com/pbs-devel/20260116112859.194016-2-s.rufinatscha@proxmox.com/
[2] - https://github.com/letsencrypt/pebble


Shan Shaji (3):
  cli: admin: make cli handling async
  fix #7179: cli: admin: expose acme commands
  chore: update proxmox-acme to version 1

 Cargo.toml            |   2 +-
 cli/admin/Cargo.toml  |   7 +-
 cli/admin/src/acme.rs | 442 ++++++++++++++++++++++++++++++++++++++++++
 cli/admin/src/main.rs |  54 ++++--
 4 files changed, 484 insertions(+), 21 deletions(-)
 create mode 100644 cli/admin/src/acme.rs

-- 
2.47.3



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


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

* [pdm-devel] [PATCH datacenter-manager 1/3] cli: admin: make cli handling async
  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 ` 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-23 17:29 ` [pdm-devel] [PATCH datacenter-manager 3/3] chore: update proxmox-acme to version 1 Shan Shaji
  2 siblings, 0 replies; 4+ messages in thread
From: Shan Shaji @ 2026-01-23 17:29 UTC (permalink / raw)
  To: pdm-devel

The acme API methods internally create workers to process API requests.
An error was thrown if the methods invoked without initializing the
worker tasks.

Since `init_worker_tasks` needs to be called from an async runtime.
Moved the content of the main function to async `run` function and
wrapped using `proxmox_async::runtime::main`, which creates a Tokio
runtime.

Signed-off-by: Shan Shaji <s.shaji@proxmox.com>
---
 cli/admin/Cargo.toml  |  3 +++
 cli/admin/src/main.rs | 51 ++++++++++++++++++++++++++-----------------
 2 files changed, 34 insertions(+), 20 deletions(-)

diff --git a/cli/admin/Cargo.toml b/cli/admin/Cargo.toml
index 0dec423..e566b39 100644
--- a/cli/admin/Cargo.toml
+++ b/cli/admin/Cargo.toml
@@ -19,6 +19,9 @@ proxmox-product-config.workspace = true
 proxmox-router = { workspace = true, features = [ "cli" ], default-features = false }
 proxmox-schema = { workspace = true, features = [ "api-macro" ] }
 proxmox-access-control.workspace = true
+proxmox-rest-server.workspace = true
+proxmox-sys.workspace = true
+proxmox-daemon.workspace = true
 
 pdm-api-types.workspace = true
 pdm-config.workspace = true
diff --git a/cli/admin/src/main.rs b/cli/admin/src/main.rs
index f698fa2..bcaa0a6 100644
--- a/cli/admin/src/main.rs
+++ b/cli/admin/src/main.rs
@@ -1,35 +1,31 @@
+use anyhow::Error;
 use serde_json::{json, Value};
 
 use proxmox_router::cli::{
-    default_table_format_options, format_and_print_result_full, get_output_format, run_cli_command,
-    CliCommand, CliCommandMap, CliEnvironment, ColumnConfig, OUTPUT_FORMAT,
+    default_table_format_options, format_and_print_result_full, get_output_format,
+    run_async_cli_command, CliCommand, CliCommandMap, CliEnvironment, ColumnConfig, OUTPUT_FORMAT,
 };
 use proxmox_router::RpcEnvironment;
-
 use proxmox_schema::api;
+use proxmox_sys::fs::CreateOptions;
 
 mod remotes;
 mod support_status;
 
-fn main() {
-    //pbs_tools::setup_libc_malloc_opts(); // TODO: move from PBS to proxmox-sys and uncomment
-
-    let api_user = pdm_config::api_user().expect("cannot get api user");
-    let priv_user = pdm_config::priv_user().expect("cannot get privileged user");
-    proxmox_product_config::init(api_user, priv_user);
+async fn run() -> Result<(), Error> {
+    let api_user = pdm_config::api_user()?;
+    let priv_user = pdm_config::priv_user()?;
 
+    proxmox_product_config::init(api_user.clone(), priv_user);
     proxmox_access_control::init::init(
         &pdm_api_types::AccessControlConfig,
         pdm_buildcfg::configdir!("/access"),
-    )
-    .expect("failed to setup access control config");
-
+    )?;
     proxmox_log::Logger::from_env("PDM_LOG", proxmox_log::LevelFilter::INFO)
         .stderr()
-        .init()
-        .expect("failed to set up logger");
+        .init()?;
 
-    server::context::init().expect("could not set up server context");
+    server::context::init()?;
 
     let cmd_def = CliCommandMap::new()
         .insert("remote", remotes::cli())
@@ -40,14 +36,29 @@ fn main() {
         .insert("support-status", support_status::cli())
         .insert("versions", CliCommand::new(&API_METHOD_GET_VERSIONS));
 
+    let args: Vec<String> = std::env::args().collect();
+    let avoid_init = args.len() >= 2 && (args[1] == "bashcomplete" || args[1] == "printdoc");
+
+    if !avoid_init {
+        let file_opts = CreateOptions::new().owner(api_user.uid).group(api_user.gid);
+        proxmox_rest_server::init_worker_tasks(pdm_buildcfg::PDM_LOG_DIR_M!().into(), file_opts)?;
+
+        let mut command_sock = proxmox_daemon::command_socket::CommandSocket::new(api_user.gid);
+        proxmox_rest_server::register_task_control_commands(&mut command_sock)?;
+        command_sock.spawn(proxmox_rest_server::last_worker_future())?;
+    }
+
     let mut rpcenv = CliEnvironment::new();
     rpcenv.set_auth_id(Some("root@pam".into()));
 
-    run_cli_command(
-        cmd_def,
-        rpcenv,
-        Some(|future| proxmox_async::runtime::main(future)),
-    );
+    run_async_cli_command(cmd_def, rpcenv).await;
+
+    Ok(())
+}
+
+fn main() -> Result<(), Error> {
+    //pbs_tools::setup_libc_malloc_opts(); // TODO: move from PBS to proxmox-sys and uncomment
+    proxmox_async::runtime::main(run())
 }
 
 #[api(
-- 
2.47.3



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


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

* [pdm-devel] [PATCH datacenter-manager 2/3] fix #7179: cli: admin: expose acme commands
  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-23 17:29 ` Shan Shaji
  2026-01-23 17:29 ` [pdm-devel] [PATCH datacenter-manager 3/3] chore: update proxmox-acme to version 1 Shan Shaji
  2 siblings, 0 replies; 4+ messages in thread
From: Shan Shaji @ 2026-01-23 17:29 UTC (permalink / raw)
  To: pdm-devel

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, },
+        }
+    }
+)]
+///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)?;
+
+                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,);
+    let location =
+        proxmox_acme_api::register_account(&name, contact, tos_url, Some(directory_url), eab_creds)
+            .await?;
+    println!("Registration successful, account URL: {}", location);
+
+    Ok(())
+}
+
+#[api(
+    input: {
+       properties: {
+           name: { type: AcmeAccountName },
+           contact:  {
+               description: "List of email addresses.",
+               type: String,
+               optional: true,
+           }
+       }
+    }
+)]
+/// Update an ACME Account.
+async fn update_account(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
+    let info = &dc_api::config::acme::API_METHOD_UPDATE_ACCOUNT;
+    let result = match info.handler {
+        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
+        _ => unreachable!(),
+    };
+
+    wait_for_local_worker(result.as_str().unwrap()).await?;
+
+    Ok(())
+}
+
+#[api(
+    input: {
+        properties: {
+            name: { type: AcmeAccountName },
+            force: {
+                description: "Delete account data even if the server refuses to deactivate the account.",
+                type: Boolean,
+                optional: true,
+                default: true,
+            }
+        }
+    }
+)]
+/// Deactivate an ACME account.
+async fn deactivate_account(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
+    let info = &dc_api::config::acme::API_METHOD_DEACTIVATE_ACCOUNT;
+    let result = match info.handler {
+        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
+        _ => unreachable!(),
+    };
+
+    wait_for_local_worker(result.as_str().unwrap()).await?;
+
+    Ok(())
+}
+
+fn account_cli() -> CommandLineInterface {
+    let cmd_def = CliCommandMap::new()
+        .insert("list", CliCommand::new(&API_METHOD_LIST_ACCOUNTS))
+        .insert(
+            "register",
+            CliCommand::new(&API_METHOD_REGISTER_ACCOUNT).arg_param(&["name", "contact"]),
+        )
+        .insert(
+            "deactivate",
+            CliCommand::new(&API_METHOD_DEACTIVATE_ACCOUNT)
+                .arg_param(&["name"])
+                .completion_cb("name", complete_acme_account),
+        )
+        .insert(
+            "info",
+            CliCommand::new(&API_METHOD_GET_ACCOUNT)
+                .arg_param(&["name"])
+                .completion_cb("name", complete_acme_account),
+        )
+        .insert(
+            "update",
+            CliCommand::new(&API_METHOD_UPDATE_ACCOUNT)
+                .arg_param(&["name", "contact"])
+                .completion_cb("name", complete_acme_account),
+        );
+
+    cmd_def.into()
+}
+
+#[api(
+    input: {
+        properties: {
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
+        }
+    }
+)]
+/// List ACME plugins.
+fn list_plugins(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
+    let output_format = get_output_format(&param);
+
+    let info = &dc_api::config::acme::API_METHOD_LIST_PLUGINS;
+    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: {
+            id: {
+                type: String,
+                description: "Plugin ID",
+            },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
+        }
+    }
+)]
+/// Show ACME plugin information.
+fn get_plugin(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
+    let output_format = get_output_format(&param);
+
+    let info = &dc_api::config::acme::API_METHOD_GET_PLUGIN;
+    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: {
+        type: {
+            type: String,
+            description: "The ACME challenge plugin type."
+        },
+        core: {
+            type: DnsPluginCore,
+            flatten: true,
+        },
+        data: {
+            type: String,
+            description: "File containing the plugin data."
+        }
+    }
+})]
+/// Add ACME plugin configuration.
+fn add_plugin(r#type: String, core: DnsPluginCore, data: String) -> Result<(), Error> {
+    let data = proxmox_base64::encode(file_get_contents(data)?);
+    dc_api::config::acme::add_plugin(r#type, core, data)?;
+    Ok(())
+}
+
+pub fn plugin_cli() -> CommandLineInterface {
+    let cmd_def = CliCommandMap::new()
+        .insert("list", CliCommand::new(&API_METHOD_LIST_PLUGINS))
+        .insert(
+            "config",
+            CliCommand::new(&API_METHOD_GET_PLUGIN)
+                .arg_param(&["id"])
+                .completion_cb("id", complete_acme_plugin),
+        )
+        .insert(
+            "add",
+            CliCommand::new(&API_METHOD_ADD_PLUGIN)
+                .arg_param(&["type", "id"])
+                .completion_cb("api", complete_acme_api_challenge_type)
+                .completion_cb("type", complete_acme_plugin_type),
+        )
+        .insert(
+            "remove",
+            CliCommand::new(&dc_api::config::acme::API_METHOD_DELETE_PLUGIN)
+                .arg_param(&["id"])
+                .completion_cb("id", complete_acme_plugin),
+        )
+        .insert(
+            "set",
+            CliCommand::new(&dc_api::config::acme::API_METHOD_UPDATE_PLUGIN)
+                .arg_param(&["id"])
+                .completion_cb("id", complete_acme_plugin),
+        );
+
+    cmd_def.into()
+}
+
+#[api(
+    input: {
+        properties: {
+            force: {
+                description: "Force renewal even if the certificate does not expire soon.",
+                type: Boolean,
+                optional: true,
+                default: false,
+            },
+        },
+    },
+)]
+/// Order a new ACME certificate.
+async fn order_acme_cert(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
+    if !param["force"].as_bool().unwrap_or(false)
+        && !dc_api::nodes::certificates::cert_expires_soon()?
+    {
+        println!("Certificate does not expire within the next 30 days, not renewing.");
+        return Ok(());
+    }
+
+    let info = &dc_api::nodes::certificates::API_METHOD_RENEW_ACME_CERT;
+    let result = match info.handler {
+        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
+        _ => unreachable!(),
+    };
+
+    wait_for_local_worker(result.as_str().unwrap()).await?;
+
+    Ok(())
+}
+
+#[api]
+/// Revoke ACME certificate.
+async fn revoke_acme_cert(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
+    let info = &dc_api::nodes::certificates::API_METHOD_REVOKE_ACME_CERT;
+    let result = match info.handler {
+        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
+        _ => unreachable!(),
+    };
+
+    wait_for_local_worker(result.as_str().unwrap()).await?;
+
+    Ok(())
+}
+
+pub fn cert_cli() -> CommandLineInterface {
+    let cmd_def = CliCommandMap::new()
+        .insert("order", CliCommand::new(&API_METHOD_ORDER_ACME_CERT))
+        .insert("revoke", CliCommand::new(&API_METHOD_REVOKE_ACME_CERT));
+
+    cmd_def.into()
+}
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()?;
 
     server::context::init()?;
 
     let cmd_def = CliCommandMap::new()
+        .insert("acme", acme::acme_mgmt_cli())
         .insert("remote", remotes::cli())
         .insert(
             "report",
-- 
2.47.3



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


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

* [pdm-devel] [PATCH datacenter-manager 3/3] chore: update proxmox-acme to version 1
  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-23 17:29 ` [pdm-devel] [PATCH datacenter-manager 2/3] fix #7179: cli: admin: expose acme commands Shan Shaji
@ 2026-01-23 17:29 ` Shan Shaji
  2 siblings, 0 replies; 4+ messages in thread
From: Shan Shaji @ 2026-01-23 17:29 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Shan Shaji <s.shaji@proxmox.com>
---
 Cargo.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Cargo.toml b/Cargo.toml
index ed9ce60..5c2de75 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -68,7 +68,7 @@ proxmox-upgrade-checks = "1"
 proxmox-uuid = "1"
 
 # other proxmox crates
-proxmox-acme = "0.5"
+proxmox-acme = "1.0"
 proxmox-openid = "1.0.2"
 
 # api implementation creates
-- 
2.47.3



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


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

end of thread, other threads:[~2026-01-23 17:29 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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-23 17:29 ` [pdm-devel] [PATCH datacenter-manager 2/3] fix #7179: cli: admin: expose acme commands Shan Shaji
2026-01-23 17:29 ` [pdm-devel] [PATCH datacenter-manager 3/3] chore: update proxmox-acme to version 1 Shan Shaji

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal