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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox