]> git.proxmox.com Git - pve-installer.git/blob - proxmox-installer-common/src/options.rs
cargo clippy --fix
[pve-installer.git] / proxmox-installer-common / src / options.rs
1 use serde::Deserialize;
2 use std::net::{IpAddr, Ipv4Addr};
3 use std::{cmp, fmt};
4
5 use crate::setup::{
6 LocaleInfo, NetworkInfo, ProductConfig, ProxmoxProduct, RuntimeInfo, SetupInfo,
7 };
8 use crate::utils::{CidrAddress, Fqdn};
9
10 #[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
11 #[serde(rename_all = "lowercase")]
12 pub enum BtrfsRaidLevel {
13 Raid0,
14 Raid1,
15 Raid10,
16 }
17
18 impl fmt::Display for BtrfsRaidLevel {
19 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
20 use BtrfsRaidLevel::*;
21 match self {
22 Raid0 => write!(f, "RAID0"),
23 Raid1 => write!(f, "RAID1"),
24 Raid10 => write!(f, "RAID10"),
25 }
26 }
27 }
28
29 #[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
30 #[serde(rename_all = "lowercase")]
31 pub enum ZfsRaidLevel {
32 Raid0,
33 Raid1,
34 Raid10,
35 #[serde(rename = "raidz-1")]
36 RaidZ,
37 #[serde(rename = "raidz-2")]
38 RaidZ2,
39 #[serde(rename = "raidz-3")]
40 RaidZ3,
41 }
42
43 impl fmt::Display for ZfsRaidLevel {
44 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
45 use ZfsRaidLevel::*;
46 match self {
47 Raid0 => write!(f, "RAID0"),
48 Raid1 => write!(f, "RAID1"),
49 Raid10 => write!(f, "RAID10"),
50 RaidZ => write!(f, "RAIDZ-1"),
51 RaidZ2 => write!(f, "RAIDZ-2"),
52 RaidZ3 => write!(f, "RAIDZ-3"),
53 }
54 }
55 }
56
57 #[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
58 #[serde(rename_all = "lowercase")]
59 pub enum FsType {
60 Ext4,
61 Xfs,
62 Zfs(ZfsRaidLevel),
63 Btrfs(BtrfsRaidLevel),
64 }
65
66 impl FsType {
67 pub fn is_btrfs(&self) -> bool {
68 matches!(self, FsType::Btrfs(_))
69 }
70 }
71
72 impl fmt::Display for FsType {
73 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
74 use FsType::*;
75 match self {
76 Ext4 => write!(f, "ext4"),
77 Xfs => write!(f, "XFS"),
78 Zfs(level) => write!(f, "ZFS ({level})"),
79 Btrfs(level) => write!(f, "Btrfs ({level})"),
80 }
81 }
82 }
83
84 #[derive(Clone, Debug)]
85 pub struct LvmBootdiskOptions {
86 pub total_size: f64,
87 pub swap_size: Option<f64>,
88 pub max_root_size: Option<f64>,
89 pub max_data_size: Option<f64>,
90 pub min_lvm_free: Option<f64>,
91 }
92
93 impl LvmBootdiskOptions {
94 pub fn defaults_from(disk: &Disk) -> Self {
95 Self {
96 total_size: disk.size,
97 swap_size: None,
98 max_root_size: None,
99 max_data_size: None,
100 min_lvm_free: None,
101 }
102 }
103 }
104
105 #[derive(Clone, Debug)]
106 pub struct BtrfsBootdiskOptions {
107 pub disk_size: f64,
108 pub selected_disks: Vec<usize>,
109 }
110
111 impl BtrfsBootdiskOptions {
112 /// This panics if the provided slice is empty.
113 pub fn defaults_from(disks: &[Disk]) -> Self {
114 let disk = &disks[0];
115 Self {
116 disk_size: disk.size,
117 selected_disks: (0..disks.len()).collect(),
118 }
119 }
120 }
121
122 #[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq)]
123 #[serde(rename_all(deserialize = "lowercase"))]
124 pub enum ZfsCompressOption {
125 #[default]
126 On,
127 Off,
128 Lzjb,
129 Lz4,
130 Zle,
131 Gzip,
132 Zstd,
133 }
134
135 impl fmt::Display for ZfsCompressOption {
136 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
137 write!(f, "{}", format!("{self:?}").to_lowercase())
138 }
139 }
140
141 impl From<&ZfsCompressOption> for String {
142 fn from(value: &ZfsCompressOption) -> Self {
143 value.to_string()
144 }
145 }
146
147 pub const ZFS_COMPRESS_OPTIONS: &[ZfsCompressOption] = {
148 use ZfsCompressOption::*;
149 &[On, Off, Lzjb, Lz4, Zle, Gzip, Zstd]
150 };
151
152 #[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq)]
153 #[serde(rename_all = "kebab-case")]
154 pub enum ZfsChecksumOption {
155 #[default]
156 On,
157 Fletcher4,
158 Sha256,
159 }
160
161 impl fmt::Display for ZfsChecksumOption {
162 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
163 write!(f, "{}", format!("{self:?}").to_lowercase())
164 }
165 }
166
167 impl From<&ZfsChecksumOption> for String {
168 fn from(value: &ZfsChecksumOption) -> Self {
169 value.to_string()
170 }
171 }
172
173 pub const ZFS_CHECKSUM_OPTIONS: &[ZfsChecksumOption] = {
174 use ZfsChecksumOption::*;
175 &[On, Fletcher4, Sha256]
176 };
177
178 #[derive(Clone, Debug)]
179 pub struct ZfsBootdiskOptions {
180 pub ashift: usize,
181 pub compress: ZfsCompressOption,
182 pub checksum: ZfsChecksumOption,
183 pub copies: usize,
184 pub arc_max: usize,
185 pub disk_size: f64,
186 pub selected_disks: Vec<usize>,
187 }
188
189 impl ZfsBootdiskOptions {
190 /// Panics if the disk list is empty.
191 pub fn defaults_from(runinfo: &RuntimeInfo, product_conf: &ProductConfig) -> Self {
192 let disk = &runinfo.disks[0];
193 Self {
194 ashift: 12,
195 compress: ZfsCompressOption::default(),
196 checksum: ZfsChecksumOption::default(),
197 copies: 1,
198 arc_max: default_zfs_arc_max(product_conf.product, runinfo.total_memory),
199 disk_size: disk.size,
200 selected_disks: (0..runinfo.disks.len()).collect(),
201 }
202 }
203 }
204
205 /// Calculates the default upper limit for the ZFS ARC size.
206 /// See also <https://bugzilla.proxmox.com/show_bug.cgi?id=4829> and
207 /// https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Module%20Parameters.html#zfs-arc-max
208 ///
209 /// # Arguments
210 /// * `product` - The product to be installed
211 /// * `total_memory` - Total memory installed in the system, in MiB
212 ///
213 /// # Returns
214 /// The default ZFS maximum ARC size in MiB for this system.
215 fn default_zfs_arc_max(product: ProxmoxProduct, total_memory: usize) -> usize {
216 if product != ProxmoxProduct::PVE {
217 // Use ZFS default for non-PVE
218 0
219 } else {
220 ((total_memory as f64) / 10.)
221 .round()
222 .clamp(64., 16. * 1024.) as usize
223 }
224 }
225
226 #[derive(Clone, Debug)]
227 pub enum AdvancedBootdiskOptions {
228 Lvm(LvmBootdiskOptions),
229 Zfs(ZfsBootdiskOptions),
230 Btrfs(BtrfsBootdiskOptions),
231 }
232
233 #[derive(Clone, Debug, Deserialize, PartialEq)]
234 pub struct Disk {
235 pub index: String,
236 pub path: String,
237 pub model: Option<String>,
238 pub size: f64,
239 pub block_size: Option<usize>,
240 }
241
242 impl fmt::Display for Disk {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 // TODO: Format sizes properly with `proxmox-human-byte` once merged
245 // https://lists.proxmox.com/pipermail/pbs-devel/2023-May/006125.html
246 f.write_str(&self.path)?;
247 if let Some(model) = &self.model {
248 // FIXME: ellipsize too-long names?
249 write!(f, " ({model})")?;
250 }
251 write!(f, " ({:.2} GiB)", self.size)
252 }
253 }
254
255 impl From<&Disk> for String {
256 fn from(value: &Disk) -> Self {
257 value.to_string()
258 }
259 }
260
261 impl cmp::Eq for Disk {}
262
263 impl cmp::PartialOrd for Disk {
264 fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
265 self.index.partial_cmp(&other.index)
266 }
267 }
268
269 impl cmp::Ord for Disk {
270 fn cmp(&self, other: &Self) -> cmp::Ordering {
271 self.index.cmp(&other.index)
272 }
273 }
274
275 #[derive(Clone, Debug)]
276 pub struct BootdiskOptions {
277 pub disks: Vec<Disk>,
278 pub fstype: FsType,
279 pub advanced: AdvancedBootdiskOptions,
280 }
281
282 impl BootdiskOptions {
283 pub fn defaults_from(disk: &Disk) -> Self {
284 Self {
285 disks: vec![disk.clone()],
286 fstype: FsType::Ext4,
287 advanced: AdvancedBootdiskOptions::Lvm(LvmBootdiskOptions::defaults_from(disk)),
288 }
289 }
290 }
291
292 #[derive(Clone, Debug)]
293 pub struct TimezoneOptions {
294 pub country: String,
295 pub timezone: String,
296 pub kb_layout: String,
297 }
298
299 impl TimezoneOptions {
300 pub fn defaults_from(runtime: &RuntimeInfo, locales: &LocaleInfo) -> Self {
301 let country = runtime.country.clone().unwrap_or_else(|| "at".to_owned());
302
303 let timezone = locales
304 .cczones
305 .get(&country)
306 .and_then(|zones| zones.first())
307 .cloned()
308 .unwrap_or_else(|| "UTC".to_owned());
309
310 let kb_layout = locales
311 .countries
312 .get(&country)
313 .and_then(|c| {
314 if c.kmap.is_empty() {
315 None
316 } else {
317 Some(c.kmap.clone())
318 }
319 })
320 .unwrap_or_else(|| "en-us".to_owned());
321
322 Self {
323 country,
324 timezone,
325 kb_layout,
326 }
327 }
328 }
329
330 #[derive(Clone, Debug)]
331 pub struct PasswordOptions {
332 pub email: String,
333 pub root_password: String,
334 }
335
336 impl Default for PasswordOptions {
337 fn default() -> Self {
338 Self {
339 email: "mail@example.invalid".to_string(),
340 root_password: String::new(),
341 }
342 }
343 }
344
345 #[derive(Clone, Debug, PartialEq)]
346 pub struct NetworkOptions {
347 pub ifname: String,
348 pub fqdn: Fqdn,
349 pub address: CidrAddress,
350 pub gateway: IpAddr,
351 pub dns_server: IpAddr,
352 }
353
354 impl NetworkOptions {
355 const DEFAULT_DOMAIN: &'static str = "example.invalid";
356
357 pub fn defaults_from(setup: &SetupInfo, network: &NetworkInfo) -> Self {
358 let mut this = Self {
359 ifname: String::new(),
360 fqdn: Self::construct_fqdn(network, setup.config.product.default_hostname()),
361 // Safety: The provided mask will always be valid.
362 address: CidrAddress::new(Ipv4Addr::UNSPECIFIED, 0).unwrap(),
363 gateway: Ipv4Addr::UNSPECIFIED.into(),
364 dns_server: Ipv4Addr::UNSPECIFIED.into(),
365 };
366
367 if let Some(ip) = network.dns.dns.first() {
368 this.dns_server = *ip;
369 }
370
371 if let Some(routes) = &network.routes {
372 let mut filled = false;
373 if let Some(gw) = &routes.gateway4 {
374 if let Some(iface) = network.interfaces.get(&gw.dev) {
375 this.ifname = iface.name.clone();
376 if let Some(addresses) = &iface.addresses {
377 if let Some(addr) = addresses.iter().find(|addr| addr.is_ipv4()) {
378 this.gateway = gw.gateway;
379 this.address = addr.clone();
380 filled = true;
381 }
382 }
383 }
384 }
385 if !filled {
386 if let Some(gw) = &routes.gateway6 {
387 if let Some(iface) = network.interfaces.get(&gw.dev) {
388 if let Some(addresses) = &iface.addresses {
389 if let Some(addr) = addresses.iter().find(|addr| addr.is_ipv6()) {
390 this.ifname = iface.name.clone();
391 this.gateway = gw.gateway;
392 this.address = addr.clone();
393 }
394 }
395 }
396 }
397 }
398 }
399
400 this
401 }
402
403 fn construct_fqdn(network: &NetworkInfo, default_hostname: &str) -> Fqdn {
404 let hostname = network.hostname.as_deref().unwrap_or(default_hostname);
405
406 let domain = network
407 .dns
408 .domain
409 .as_deref()
410 .unwrap_or(Self::DEFAULT_DOMAIN);
411
412 Fqdn::from(&format!("{hostname}.{domain}")).unwrap_or_else(|_| {
413 // Safety: This will always result in a valid FQDN, as we control & know
414 // the values of default_hostname (one of "pve", "pmg" or "pbs") and
415 // constant-defined DEFAULT_DOMAIN.
416 Fqdn::from(&format!("{}.{}", default_hostname, Self::DEFAULT_DOMAIN)).unwrap()
417 })
418 }
419 }
420
421 #[cfg(test)]
422 mod tests {
423 use super::*;
424
425 #[test]
426 fn zfs_arc_limit() {
427 const TESTS: &[(usize, usize)] = &[
428 (16, 64), // at least 64 MiB
429 (1024, 102),
430 (4 * 1024, 410),
431 (8 * 1024, 819),
432 (150 * 1024, 15360),
433 (160 * 1024, 16384),
434 (1024 * 1024, 16384), // maximum of 16 GiB
435 ];
436
437 for (total_memory, expected) in TESTS {
438 assert_eq!(
439 default_zfs_arc_max(ProxmoxProduct::PVE, *total_memory),
440 *expected
441 );
442 assert_eq!(default_zfs_arc_max(ProxmoxProduct::PBS, *total_memory), 0);
443 assert_eq!(default_zfs_arc_max(ProxmoxProduct::PMG, *total_memory), 0);
444 }
445 }
446 }