From: Wolfgang Bumiller <w.bumiller@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] [PATCH v2 backup 21/27] add acme commands to proxmox-backup-manager
Date: Thu, 22 Apr 2021 16:02:07 +0200 [thread overview]
Message-ID: <20210422140213.30989-22-w.bumiller@proxmox.com> (raw)
In-Reply-To: <20210422140213.30989-1-w.bumiller@proxmox.com>
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
src/bin/proxmox-backup-manager.rs | 1 +
src/bin/proxmox_backup_manager/acme.rs | 415 +++++++++++++++++++++++++
src/bin/proxmox_backup_manager/mod.rs | 2 +
3 files changed, 418 insertions(+)
create mode 100644 src/bin/proxmox_backup_manager/acme.rs
diff --git a/src/bin/proxmox-backup-manager.rs b/src/bin/proxmox-backup-manager.rs
index 105a11f8..522c800e 100644
--- a/src/bin/proxmox-backup-manager.rs
+++ b/src/bin/proxmox-backup-manager.rs
@@ -355,6 +355,7 @@ fn main() {
.insert("user", user_commands())
.insert("remote", remote_commands())
.insert("garbage-collection", garbage_collection_commands())
+ .insert("acme", acme_mgmt_cli())
.insert("cert", cert_mgmt_cli())
.insert("subscription", subscription_commands())
.insert("sync-job", sync_job_commands())
diff --git a/src/bin/proxmox_backup_manager/acme.rs b/src/bin/proxmox_backup_manager/acme.rs
new file mode 100644
index 00000000..317473cb
--- /dev/null
+++ b/src/bin/proxmox_backup_manager/acme.rs
@@ -0,0 +1,415 @@
+use std::io::Write;
+
+use anyhow::{bail, Error};
+use serde_json::Value;
+
+use proxmox::api::{api, cli::*, ApiHandler, RpcEnvironment};
+use proxmox::tools::fs::file_get_contents;
+
+use proxmox_backup::acme::AcmeClient;
+use proxmox_backup::api2;
+use proxmox_backup::config::acme::plugin::DnsPluginCoreUpdater;
+use proxmox_backup::config::acme::{AccountName, KNOWN_ACME_DIRECTORIES};
+
+pub fn acme_mgmt_cli() -> CommandLineInterface {
+ let cmd_def = CliCommandMap::new()
+ .insert("account", account_cli())
+ .insert("cert", cert_cli())
+ .insert("plugin", plugin_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 = &api2::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: AccountName },
+ "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 = &api2::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: AccountName },
+ contact: {
+ description: "List of email addresses.",
+ },
+ directory: {
+ type: String,
+ description: "The ACME Directory.",
+ optional: true,
+ },
+ }
+ }
+)]
+/// Register an ACME account.
+async fn register_account(
+ name: AccountName,
+ contact: String,
+ directory: Option<String>,
+) -> Result<(), Error> {
+ let directory = match directory {
+ Some(directory) => directory,
+ 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_owned();
+ }
+ Ok(n) if n == KNOWN_ACME_DIRECTORIES.len() => {
+ input.clear();
+ std::io::stdin().read_line(&mut input)?;
+ break input.trim().to_owned();
+ }
+ _ => eprintln!("Invalid selection."),
+ }
+
+ attempt += 1;
+ if attempt >= 3 {
+ bail!("Aborting.");
+ }
+ }
+ }
+ };
+
+ println!("Attempting to fetch Terms of Service from {:?}", directory);
+ let mut client = AcmeClient::new(directory.clone());
+ let tos_agreed = if let Some(tos_url) = client.terms_of_service_url().await? {
+ 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)?;
+ if input.trim().eq_ignore_ascii_case("y") {
+ true
+ } else {
+ false
+ }
+ } else {
+ false
+ };
+
+ println!("Attempting to register account with {:?}...", directory);
+
+ let account =
+ api2::config::acme::do_register_account(&mut client, &name, tos_agreed, contact, None)
+ .await?;
+
+ println!("Registration successful, account URL: {}", account.location);
+
+ Ok(())
+}
+
+#[api(
+ input: {
+ properties: {
+ name: { type: AccountName },
+ 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 = &api2::config::acme::API_METHOD_UPDATE_ACCOUNT;
+ let result = match info.handler {
+ ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
+ _ => unreachable!(),
+ };
+
+ crate::wait_for_local_worker(result.as_str().unwrap()).await?;
+
+ Ok(())
+}
+
+#[api(
+ input: {
+ properties: {
+ name: { type: AccountName },
+ force: {
+ description:
+ "Delete account data even if the server refuses to deactivate the account.",
+ type: Boolean,
+ optional: true,
+ default: false,
+ },
+ }
+ }
+)]
+/// Deactivate an ACME account.
+async fn deactivate_account(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
+ let info = &api2::config::acme::API_METHOD_DEACTIVATE_ACCOUNT;
+ let result = match info.handler {
+ ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
+ _ => unreachable!(),
+ };
+
+ crate::wait_for_local_worker(result.as_str().unwrap()).await?;
+
+ Ok(())
+}
+
+pub 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", crate::config::acme::complete_acme_account),
+ )
+ .insert(
+ "info",
+ CliCommand::new(&API_METHOD_GET_ACCOUNT)
+ .arg_param(&["name"])
+ .completion_cb("name", crate::config::acme::complete_acme_account),
+ )
+ .insert(
+ "update",
+ CliCommand::new(&API_METHOD_UPDATE_ACCOUNT)
+ .arg_param(&["name"])
+ .completion_cb("name", crate::config::acme::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(¶m);
+
+ let info = &api2::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 account information.
+fn get_plugin(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
+ let output_format = get_output_format(¶m);
+
+ let info = &api2::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: DnsPluginCoreUpdater,
+ flatten: true,
+ },
+ data: {
+ type: String,
+ description: "File containing the plugin data.",
+ },
+ }
+ }
+)]
+/// Show acme account information.
+fn add_plugin(r#type: String, core: DnsPluginCoreUpdater, data: String) -> Result<(), Error> {
+ let data = base64::encode(&file_get_contents(&data)?);
+ api2::config::acme::add_plugin(r#type, core, data)?;
+ Ok(())
+}
+
+pub fn plugin_cli() -> CommandLineInterface {
+ use proxmox_backup::api2::config::acme;
+ let cmd_def = CliCommandMap::new()
+ .insert("list", CliCommand::new(&API_METHOD_LIST_PLUGINS))
+ .insert(
+ "config", // name comes from pve/pmg
+ CliCommand::new(&API_METHOD_GET_PLUGIN)
+ .arg_param(&["id"])
+ .completion_cb("id", crate::config::acme::complete_acme_plugin),
+ )
+ .insert(
+ "add",
+ CliCommand::new(&API_METHOD_ADD_PLUGIN)
+ .arg_param(&["type", "id"])
+ .completion_cb("id", crate::config::acme::complete_acme_plugin)
+ .completion_cb("type", crate::config::acme::complete_acme_plugin_type),
+ )
+ .insert(
+ "remove",
+ CliCommand::new(&acme::API_METHOD_DELETE_PLUGIN)
+ .arg_param(&["id"])
+ .completion_cb("id", crate::config::acme::complete_acme_plugin),
+ )
+ .insert(
+ "set",
+ CliCommand::new(&acme::API_METHOD_UPDATE_PLUGIN)
+ .arg_param(&["id"])
+ .completion_cb("id", crate::config::acme::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) && !api2::node::certificates::cert_expires_soon()?
+ {
+ println!("Certificate does not expire within the next 30 days, not renewing.");
+ return Ok(());
+ }
+
+ let info = &api2::node::certificates::API_METHOD_RENEW_ACME_CERT;
+ let result = match info.handler {
+ ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
+ _ => unreachable!(),
+ };
+
+ crate::wait_for_local_worker(result.as_str().unwrap()).await?;
+
+ Ok(())
+}
+
+#[api]
+/// Order a new ACME certificate.
+async fn revoke_acme_cert(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
+ let info = &api2::node::certificates::API_METHOD_REVOKE_ACME_CERT;
+ let result = match info.handler {
+ ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
+ _ => unreachable!(),
+ };
+
+ crate::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/src/bin/proxmox_backup_manager/mod.rs b/src/bin/proxmox_backup_manager/mod.rs
index 900144aa..e574e4d4 100644
--- a/src/bin/proxmox_backup_manager/mod.rs
+++ b/src/bin/proxmox_backup_manager/mod.rs
@@ -1,5 +1,7 @@
mod acl;
pub use acl::*;
+mod acme;
+pub use acme::*;
mod cert;
pub use cert::*;
mod datastore;
--
2.20.1
next prev parent reply other threads:[~2021-04-22 14:03 UTC|newest]
Thread overview: 62+ messages / expand[flat|nested] mbox.gz Atom feed top
2021-04-22 14:01 [pbs-devel] [PATCH v2 backup 00/27] Implements ACME support for PBS Wolfgang Bumiller
2021-04-22 14:01 ` [pbs-devel] [PATCH v2 backup 01/27] systemd: add reload_unit Wolfgang Bumiller
2021-04-28 10:15 ` [pbs-devel] applied: " Dietmar Maurer
2021-04-22 14:01 ` [pbs-devel] [PATCH v2 backup 02/27] add dns alias schema Wolfgang Bumiller
2021-04-28 10:26 ` Dietmar Maurer
2021-04-28 11:07 ` Wolfgang Bumiller
2021-04-29 10:20 ` [pbs-devel] applied: " Dietmar Maurer
2021-04-22 14:01 ` [pbs-devel] [PATCH v2 backup 03/27] tools::fs::scan_subdir: use nix::Error instead of anyhow Wolfgang Bumiller
2021-04-28 10:36 ` [pbs-devel] applied: " Dietmar Maurer
2021-04-22 14:01 ` [pbs-devel] [PATCH v2 backup 04/27] config: factor out certificate writing Wolfgang Bumiller
2021-04-28 10:59 ` [pbs-devel] applied: " Dietmar Maurer
2021-04-22 14:01 ` [pbs-devel] [PATCH v2 backup 05/27] CertInfo: add not_{after, before}_unix Wolfgang Bumiller
2021-04-28 11:05 ` Dietmar Maurer
2021-04-28 11:12 ` Wolfgang Bumiller
2021-04-29 6:13 ` Dietmar Maurer
2021-04-29 7:01 ` Wolfgang Bumiller
2021-04-29 7:08 ` Dietmar Maurer
2021-04-29 7:14 ` Wolfgang Bumiller
2021-04-29 8:33 ` Dietmar Maurer
2021-04-29 8:49 ` Wolfgang Bumiller
2021-04-29 9:06 ` [pbs-devel] applied: " Dietmar Maurer
2021-04-22 14:01 ` [pbs-devel] [PATCH v2 backup 06/27] CertInfo: add is_expired_after_epoch Wolfgang Bumiller
2021-04-29 9:11 ` [pbs-devel] applied: " Dietmar Maurer
2021-04-22 14:01 ` [pbs-devel] [PATCH v2 backup 07/27] tools: add ControlFlow type Wolfgang Bumiller
2021-04-29 9:17 ` [pbs-devel] applied: " Dietmar Maurer
2021-04-29 9:26 ` Wolfgang Bumiller
2021-04-22 14:01 ` [pbs-devel] [PATCH v2 backup 08/27] catalog shell: replace LoopState with ControlFlow Wolfgang Bumiller
2021-04-29 9:17 ` [pbs-devel] applied: " Dietmar Maurer
2021-04-22 14:01 ` [pbs-devel] [PATCH v2 backup 09/27] Cargo.toml: depend on proxmox-acme-rs Wolfgang Bumiller
2021-04-29 10:07 ` [pbs-devel] applied: " Dietmar Maurer
2021-04-22 14:01 ` [pbs-devel] [PATCH v2 backup 10/27] bump d/control Wolfgang Bumiller
2021-04-29 10:07 ` [pbs-devel] applied: " Dietmar Maurer
2021-04-22 14:01 ` [pbs-devel] [PATCH v2 backup 11/27] config::acl: make /system/certificates a valid path Wolfgang Bumiller
2021-04-29 10:08 ` [pbs-devel] applied: " Dietmar Maurer
2021-04-22 14:01 ` [pbs-devel] [PATCH v2 backup 12/27] add 'config file format' to tools::config Wolfgang Bumiller
2021-04-29 10:12 ` [pbs-devel] applied: " Dietmar Maurer
2021-04-22 14:01 ` [pbs-devel] [PATCH v2 backup 13/27] add node config Wolfgang Bumiller
2021-04-29 10:39 ` Dietmar Maurer
2021-04-29 12:40 ` Dietmar Maurer
2021-04-29 13:15 ` Wolfgang Bumiller
2021-04-22 14:02 ` [pbs-devel] [PATCH v2 backup 14/27] add acme config Wolfgang Bumiller
2021-04-29 10:48 ` Dietmar Maurer
2021-04-29 11:36 ` Wolfgang Bumiller
2021-04-29 10:53 ` Dietmar Maurer
2021-04-29 11:34 ` Wolfgang Bumiller
2021-04-22 14:02 ` [pbs-devel] [PATCH v2 backup 15/27] tools/http: dedup user agent string Wolfgang Bumiller
2021-04-28 10:37 ` Dietmar Maurer
2021-04-22 14:02 ` [pbs-devel] [PATCH v2 backup 16/27] tools/http: add request_with_agent helper Wolfgang Bumiller
2021-04-28 10:38 ` Dietmar Maurer
2021-04-22 14:02 ` [pbs-devel] [PATCH v2 backup 17/27] add async acme client implementation Wolfgang Bumiller
2021-04-22 14:02 ` [pbs-devel] [PATCH v2 backup 18/27] add config/acme api path Wolfgang Bumiller
2021-04-22 14:02 ` [pbs-devel] [PATCH v2 backup 19/27] add node/{node}/certificates api call Wolfgang Bumiller
2021-04-22 14:02 ` [pbs-devel] [PATCH v2 backup 20/27] add node/{node}/config api path Wolfgang Bumiller
2021-04-22 14:02 ` Wolfgang Bumiller [this message]
2021-04-22 14:02 ` [pbs-devel] [PATCH v2 backup 22/27] implement standalone acme validation Wolfgang Bumiller
2021-04-22 14:02 ` [pbs-devel] [PATCH v2 backup 23/27] ui: add certificate & acme view Wolfgang Bumiller
2021-04-22 14:02 ` [pbs-devel] [PATCH v2 backup 24/27] daily-update: check acme certificates Wolfgang Bumiller
2021-04-22 14:02 ` [pbs-devel] [PATCH v2 backup 25/27] acme: create directories as needed Wolfgang Bumiller
2021-04-22 14:12 ` Wolfgang Bumiller
2021-04-22 14:02 ` [pbs-devel] [PATCH v2 backup 26/27] acme: pipe plugin output to task log Wolfgang Bumiller
2021-04-22 14:02 ` [pbs-devel] [PATCH v2 backup 27/27] api: acme: make account name optional in register call Wolfgang Bumiller
2021-04-23 10:43 ` [pbs-devel] [PATCH v2 backup 00/27] Implements ACME support for PBS Dominic Jäger
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=20210422140213.30989-22-w.bumiller@proxmox.com \
--to=w.bumiller@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 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.