From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 9D53573ADE for ; Fri, 16 Apr 2021 15:35:54 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 160C224FDA for ; Fri, 16 Apr 2021 15:35:33 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 9F8B024CA2 for ; Fri, 16 Apr 2021 15:35:24 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 495BA43E58 for ; Fri, 16 Apr 2021 15:35:24 +0200 (CEST) From: Wolfgang Bumiller To: pbs-devel@lists.proxmox.com Date: Fri, 16 Apr 2021 15:35:07 +0200 Message-Id: <20210416133517.23349-15-w.bumiller@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20210416133517.23349-1-w.bumiller@proxmox.com> References: <20210416133517.23349-1-w.bumiller@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.031 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [config.rs, node.rs] Subject: [pbs-devel] [RFC backup 14/23] add node config X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Fri, 16 Apr 2021 13:35:54 -0000 Signed-off-by: Wolfgang Bumiller --- src/config.rs | 1 + src/config/node.rs | 225 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 src/config/node.rs diff --git a/src/config.rs b/src/config.rs index 37df2fd2..717829e2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,6 +19,7 @@ pub mod acl; pub mod cached_user_info; pub mod datastore; pub mod network; +pub mod node; pub mod remote; pub mod sync; pub mod tfa; diff --git a/src/config/node.rs b/src/config/node.rs new file mode 100644 index 00000000..c85b5128 --- /dev/null +++ b/src/config/node.rs @@ -0,0 +1,225 @@ +use std::fs::File; +use std::time::Duration; + +use anyhow::{format_err, Error}; +use nix::sys::stat::Mode; +use serde::{Deserialize, Serialize}; + +use proxmox::api::api; +use proxmox::api::schema::{self, Updater}; +use proxmox::tools::fs::{replace_file, CreateOptions}; + +use crate::acme::AcmeClient; +use crate::api2::types::{DNS_ALIAS_FORMAT, DNS_NAME_FORMAT, PROXMOX_SAFE_ID_FORMAT}; +use crate::config::acme::AccountName; + +const CONF_FILE: &str = configdir!("/node.cfg"); +const LOCK_FILE: &str = configdir!("/.node.cfg.lock"); +const LOCK_TIMEOUT: Duration = Duration::from_secs(5); + +pub fn read_lock() -> Result { + proxmox::tools::fs::open_file_locked(LOCK_FILE, LOCK_TIMEOUT, false) +} + +pub fn write_lock() -> Result { + proxmox::tools::fs::open_file_locked(LOCK_FILE, LOCK_TIMEOUT, true) +} + +/// Read the Node Config. +pub fn config() -> Result<(NodeConfig, [u8; 32]), Error> { + let content = + proxmox::tools::fs::file_read_optional_string(CONF_FILE)?.unwrap_or_else(|| "".to_string()); + + let digest = openssl::sha::sha256(content.as_bytes()); + let data: NodeConfig = crate::tools::config::from_str(&content, &NodeConfig::API_SCHEMA)?; + + Ok((data, digest)) +} + +/// Write the Node Config, requires the write lock to be held. +pub fn save_config(config: &NodeConfig) -> Result<(), Error> { + let raw = crate::tools::config::to_bytes(config, &NodeConfig::API_SCHEMA)?; + + let backup_user = crate::backup::backup_user()?; + let options = CreateOptions::new() + .perm(Mode::from_bits_truncate(0o0640)) + .owner(nix::unistd::ROOT) + .group(backup_user.gid); + + replace_file(CONF_FILE, &raw, options) +} + +#[api( + properties: { + "domain": { format: &DNS_NAME_FORMAT }, + "alias": { + optional: true, + format: &DNS_ALIAS_FORMAT, + }, + "plugin": { + optional: true, + format: &PROXMOX_SAFE_ID_FORMAT, + }, + }, + default_key: "domain", +)] +#[derive(Deserialize, Serialize)] +/// A domain entry for an ACME certificate. +pub struct AcmeDomain { + /// The domain to certify for. + pub domain: String, + + /// The domain to use for challenges instead of the default acme challenge domain. + /// + /// This is useful if you use CNAME entries to redirect `_acme-challenge.*` domains to a + /// different DNS server. + #[serde(skip_serializing_if = "Option::is_none")] + pub alias: Option, + + /// The plugin to use to validate this domain. + /// + /// Empty means standalone HTTP validation is used. + #[serde(skip_serializing_if = "Option::is_none")] + pub plugin: Option, +} + +#[api( + properties: { + account: { type: AccountName }, + } +)] +#[derive(Deserialize, Serialize)] +/// The ACME configuration. +/// +/// Currently only contains the name of the account use. +pub struct AcmeConfig { + /// Account to use to acquire ACME certificates. + account: AccountName, +} + +#[api( + properties: { + acme: { + optional: true, + type: String, + format: &schema::ApiStringFormat::PropertyString(&AcmeConfig::API_SCHEMA), + }, + acmedomain0: { + type: String, + optional: true, + format: &schema::ApiStringFormat::PropertyString(&AcmeDomain::API_SCHEMA), + }, + acmedomain1: { + type: String, + optional: true, + format: &schema::ApiStringFormat::PropertyString(&AcmeDomain::API_SCHEMA), + }, + acmedomain2: { + type: String, + optional: true, + format: &schema::ApiStringFormat::PropertyString(&AcmeDomain::API_SCHEMA), + }, + acmedomain3: { + type: String, + optional: true, + format: &schema::ApiStringFormat::PropertyString(&AcmeDomain::API_SCHEMA), + }, + acmedomain4: { + type: String, + optional: true, + format: &schema::ApiStringFormat::PropertyString(&AcmeDomain::API_SCHEMA), + }, + }, +)] +#[derive(Deserialize, Serialize, Updater)] +/// Node specific configuration. +pub struct NodeConfig { + /// The acme account to use on this node. + #[serde(skip_serializing_if = "Updater::is_empty")] + acme: Option, + + /// ACME domain to get a certificate for for this node. + #[serde(skip_serializing_if = "Updater::is_empty")] + acmedomain0: Option, + + /// ACME domain to get a certificate for for this node. + #[serde(skip_serializing_if = "Updater::is_empty")] + acmedomain1: Option, + + /// ACME domain to get a certificate for for this node. + #[serde(skip_serializing_if = "Updater::is_empty")] + acmedomain2: Option, + + /// ACME domain to get a certificate for for this node. + #[serde(skip_serializing_if = "Updater::is_empty")] + acmedomain3: Option, + + /// ACME domain to get a certificate for for this node. + #[serde(skip_serializing_if = "Updater::is_empty")] + acmedomain4: Option, +} + +impl NodeConfig { + pub fn acme_config(&self) -> Option> { + self.acme.as_deref().map(|config| -> Result<_, Error> { + Ok(crate::tools::config::from_property_string( + config, + &AcmeConfig::API_SCHEMA, + )?) + }) + } + + pub async fn acme_client(&self) -> Result { + AcmeClient::load( + &self + .acme_config() + .ok_or_else(|| format_err!("no acme client configured"))?? + .account, + ) + .await + } + + pub fn acme_domains(&self) -> AcmeDomainIter { + AcmeDomainIter::new(self) + } +} + +pub struct AcmeDomainIter<'a> { + config: &'a NodeConfig, + index: usize, +} + +impl<'a> AcmeDomainIter<'a> { + fn new(config: &'a NodeConfig) -> Self { + Self { config, index: 0 } + } +} + +impl<'a> Iterator for AcmeDomainIter<'a> { + type Item = Result; + + fn next(&mut self) -> Option { + let domain = loop { + let index = self.index; + self.index += 1; + + let domain = match index { + 0 => self.config.acmedomain0.as_deref(), + 1 => self.config.acmedomain1.as_deref(), + 2 => self.config.acmedomain2.as_deref(), + 3 => self.config.acmedomain3.as_deref(), + 4 => self.config.acmedomain4.as_deref(), + _ => return None, + }; + + if let Some(domain) = domain { + break domain; + } + }; + + Some(crate::tools::config::from_property_string( + domain, + &AcmeDomain::API_SCHEMA, + )) + } +} -- 2.20.1