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