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