public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Wolfgang Bumiller <w.bumiller@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] [REBASED v2 backup 7/9] add acme commands to proxmox-backup-manager
Date: Mon,  3 May 2021 11:39:57 +0200	[thread overview]
Message-ID: <20210503093959.14855-8-w.bumiller@proxmox.com> (raw)
In-Reply-To: <20210503093959.14855-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(&param);
+
+    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(&param);
+
+    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(&param);
+
+    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(&param);
+
+    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





  parent reply	other threads:[~2021-05-03  9:40 UTC|newest]

Thread overview: 12+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-05-03  9:39 [pbs-devel] [REBASED v2 backup 0/9] rebased and reordered acme implementation Wolfgang Bumiller
2021-05-03  9:39 ` [pbs-devel] [REBASED v2 backup 1/9] add acme config Wolfgang Bumiller
2021-05-03  9:39 ` [pbs-devel] [REBASED v2 backup 2/9] add acme client Wolfgang Bumiller
2021-05-04  6:10   ` Dietmar Maurer
2021-05-03  9:39 ` [pbs-devel] [REBASED v2 backup 3/9] add node config Wolfgang Bumiller
2021-05-03  9:39 ` [pbs-devel] [REBASED v2 backup 4/9] add config/acme api path Wolfgang Bumiller
2021-05-03  9:39 ` [pbs-devel] [REBASED v2 backup 5/9] add node/{node}/certificates api call Wolfgang Bumiller
2021-05-03  9:39 ` [pbs-devel] [REBASED v2 backup 6/9] add node/{node}/config api path Wolfgang Bumiller
2021-05-03  9:39 ` Wolfgang Bumiller [this message]
2021-05-03  9:39 ` [pbs-devel] [REBASED v2 backup 8/9] ui: add certificate & acme view Wolfgang Bumiller
2021-05-03  9:39 ` [pbs-devel] [REBASED v2 backup 9/9] daily-update: check acme certificates Wolfgang Bumiller
2021-05-04  7:57 ` [pbs-devel] applied: [REBASED v2 backup 0/9] rebased and reordered acme implementation Dietmar Maurer

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=20210503093959.14855-8-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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal