]> git.proxmox.com Git - proxmox-backup.git/blob - src/config/node.rs
15b153a1dd5ed52f3c5c6814136b59dc7f068e5d
[proxmox-backup.git] / src / config / node.rs
1 use std::collections::HashSet;
2
3 use anyhow::{bail, Error};
4 use serde::{Deserialize, Serialize};
5
6 use proxmox::api::api;
7 use proxmox::api::schema::{ApiStringFormat, ApiType, Updater};
8
9 use proxmox_http::ProxyConfig;
10
11 use pbs_buildcfg::configdir;
12 use pbs_config::{open_backup_lockfile, BackupLockGuard};
13
14 use crate::acme::AcmeClient;
15 use crate::api2::types::{
16 AcmeAccountName, AcmeDomain, ACME_DOMAIN_PROPERTY_SCHEMA, HTTP_PROXY_SCHEMA,
17 };
18
19 const CONF_FILE: &str = configdir!("/node.cfg");
20 const LOCK_FILE: &str = configdir!("/.node.lck");
21
22 pub fn lock() -> Result<BackupLockGuard, Error> {
23 open_backup_lockfile(LOCK_FILE, None, true)
24 }
25
26 /// Read the Node Config.
27 pub fn config() -> Result<(NodeConfig, [u8; 32]), Error> {
28 let content =
29 proxmox::tools::fs::file_read_optional_string(CONF_FILE)?.unwrap_or_else(|| "".to_string());
30
31 let digest = openssl::sha::sha256(content.as_bytes());
32 let data: NodeConfig = crate::tools::config::from_str(&content, &NodeConfig::API_SCHEMA)?;
33
34 Ok((data, digest))
35 }
36
37 /// Write the Node Config, requires the write lock to be held.
38 pub fn save_config(config: &NodeConfig) -> Result<(), Error> {
39 config.validate()?;
40
41 let raw = crate::tools::config::to_bytes(config, &NodeConfig::API_SCHEMA)?;
42 pbs_config::replace_backup_config(CONF_FILE, &raw)
43 }
44
45 #[api(
46 properties: {
47 account: { type: AcmeAccountName },
48 }
49 )]
50 #[derive(Deserialize, Serialize)]
51 /// The ACME configuration.
52 ///
53 /// Currently only contains the name of the account use.
54 pub struct AcmeConfig {
55 /// Account to use to acquire ACME certificates.
56 account: AcmeAccountName,
57 }
58
59 #[api(
60 properties: {
61 acme: {
62 optional: true,
63 type: String,
64 format: &ApiStringFormat::PropertyString(&AcmeConfig::API_SCHEMA),
65 },
66 acmedomain0: {
67 schema: ACME_DOMAIN_PROPERTY_SCHEMA,
68 optional: true,
69 },
70 acmedomain1: {
71 schema: ACME_DOMAIN_PROPERTY_SCHEMA,
72 optional: true,
73 },
74 acmedomain2: {
75 schema: ACME_DOMAIN_PROPERTY_SCHEMA,
76 optional: true,
77 },
78 acmedomain3: {
79 schema: ACME_DOMAIN_PROPERTY_SCHEMA,
80 optional: true,
81 },
82 acmedomain4: {
83 schema: ACME_DOMAIN_PROPERTY_SCHEMA,
84 optional: true,
85 },
86 "http-proxy": {
87 schema: HTTP_PROXY_SCHEMA,
88 optional: true,
89 },
90 },
91 )]
92 #[derive(Deserialize, Serialize, Updater)]
93 #[serde(rename_all = "kebab-case")]
94 /// Node specific configuration.
95 pub struct NodeConfig {
96 /// The acme account to use on this node.
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub acme: Option<String>,
99
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub acmedomain0: Option<String>,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub acmedomain1: Option<String>,
105
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub acmedomain2: Option<String>,
108
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub acmedomain3: Option<String>,
111
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub acmedomain4: Option<String>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub http_proxy: Option<String>,
117 }
118
119 impl NodeConfig {
120 pub fn acme_config(&self) -> Option<Result<AcmeConfig, Error>> {
121 self.acme.as_deref().map(|config| -> Result<_, Error> {
122 Ok(crate::tools::config::from_property_string(
123 config,
124 &AcmeConfig::API_SCHEMA,
125 )?)
126 })
127 }
128
129 pub async fn acme_client(&self) -> Result<AcmeClient, Error> {
130 let account = if let Some(cfg) = self.acme_config().transpose()? {
131 cfg.account
132 } else {
133 AcmeAccountName::from_string("default".to_string())? // should really not happen
134 };
135 AcmeClient::load(&account).await
136 }
137
138 pub fn acme_domains(&self) -> AcmeDomainIter {
139 AcmeDomainIter::new(self)
140 }
141
142 /// Returns the parsed ProxyConfig
143 pub fn http_proxy(&self) -> Option<ProxyConfig> {
144 if let Some(http_proxy) = &self.http_proxy {
145 match ProxyConfig::parse_proxy_url(&http_proxy) {
146 Ok(proxy) => Some(proxy),
147 Err(_) => None,
148 }
149 } else {
150 None
151 }
152 }
153
154 /// Sets the HTTP proxy configuration
155 pub fn set_http_proxy(&mut self, http_proxy: Option<String>) {
156 self.http_proxy = http_proxy;
157 }
158
159 /// Validate the configuration.
160 pub fn validate(&self) -> Result<(), Error> {
161 let mut domains = HashSet::new();
162 for domain in self.acme_domains() {
163 let domain = domain?;
164 if !domains.insert(domain.domain.to_lowercase()) {
165 bail!("duplicate domain '{}' in ACME config", domain.domain);
166 }
167 }
168
169 Ok(())
170 }
171 }
172
173 pub struct AcmeDomainIter<'a> {
174 config: &'a NodeConfig,
175 index: usize,
176 }
177
178 impl<'a> AcmeDomainIter<'a> {
179 fn new(config: &'a NodeConfig) -> Self {
180 Self { config, index: 0 }
181 }
182 }
183
184 impl<'a> Iterator for AcmeDomainIter<'a> {
185 type Item = Result<AcmeDomain, Error>;
186
187 fn next(&mut self) -> Option<Self::Item> {
188 let domain = loop {
189 let index = self.index;
190 self.index += 1;
191
192 let domain = match index {
193 0 => self.config.acmedomain0.as_deref(),
194 1 => self.config.acmedomain1.as_deref(),
195 2 => self.config.acmedomain2.as_deref(),
196 3 => self.config.acmedomain3.as_deref(),
197 4 => self.config.acmedomain4.as_deref(),
198 _ => return None,
199 };
200
201 if let Some(domain) = domain {
202 break domain;
203 }
204 };
205
206 Some(crate::tools::config::from_property_string(
207 domain,
208 &AcmeDomain::API_SCHEMA,
209 ))
210 }
211 }