public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH installer v2 0/6] add early disk and network sanity checks
@ 2025-04-29 14:09 Michael Köppl
  2025-04-29 14:09 ` [pve-devel] [PATCH pve-installer v2 1/6] auto: add early answer file sanity check for RAID configurations Michael Köppl
                   ` (5 more replies)
  0 siblings, 6 replies; 11+ messages in thread
From: Michael Köppl @ 2025-04-29 14:09 UTC (permalink / raw)
  To: pve-devel

The goal of this series is to add additional sanity checks to the
auto-installer and the TUI and GUI installers. The following checks were
added:
* Btrfs / ZFS RAID: check if the required number of disks is available
* LVM: check if swapsize < hdsize
* LVM: check if maxroot < hdsize/4
* Duplicate disks in answer file disk selection
* Networking: check if IPv4 address is valid within subnet (RFC)

The disk-related checks aim to close [0], whereas the IPv4 check would
partially close [1].

[0] https://bugzilla.proxmox.com/show_bug.cgi?id=5887
[1] https://bugzilla.proxmox.com/show_bug.cgi?id=5757

Changes since v1:
- Removed rustfmt patch
- Removed patch that changed check_zfs_raid_config (replaced with patch
  below)
- Added patch that moves implementation of RAID setup checks to 
  implementation of ZfsRaidLevel and BtrfsRaidLevel
- Adapted tests for RAID setup validity
- Added configured values in GiB to error messages for invalid 
  swapsize and maxroot
- Changed maximum allowed swapsize to hdsize/8
- De-duplicated swapsize and maxroot check implementations in
  autoinstaller utils and common's disk_checks 
- Added patch that adds a check for duplicate disks in answer file disk
  selection
- Refactored Display implementation of CidrAddressParseError
- Simplified Deserialize implementation of CidrAddress
- Explicitly allow /31 and /32 subnets in CIDR validity check

Thanks to @Christoph for reviewing v1 and his suggestions.

pve-installer:

Michael Köppl (6):
  auto: add early answer file sanity check for RAID configurations
  move RAID setup checks to RAID level enum implementations
  close #5887: add sanity check for LVM swapsize and maxroot
  auto: add check for duplicate disks in answer file
  common: add more descriptive errors for invalid network configs
  common: add checks for valid subnet mask and IPv4 address within
    subnet

 Proxmox/Install.pm                            |  16 ++
 proxinstall                                   |   5 +-
 proxmox-auto-install-assistant/src/main.rs    |   7 +-
 proxmox-auto-installer/src/utils.rs           |  46 ++++-
 proxmox-auto-installer/tests/parse-answer.rs  |  12 +-
 .../parse_answer/ipv4_and_subnet_31.json      |  19 ++
 .../parse_answer/ipv4_and_subnet_31.toml      |  18 ++
 .../btrfs_raid_single_disk.json               |   3 +
 .../btrfs_raid_single_disk.toml               |  15 ++
 .../parse_answer_fail/duplicate_disk.json     |   3 +
 .../parse_answer_fail/duplicate_disk.toml     |  15 ++
 .../ipv4_and_subnet_addr_is_network.json      |   3 +
 .../ipv4_and_subnet_addr_is_network.toml      |  18 ++
 .../ipv4_and_subnet_mask_33.json              |   3 +
 .../ipv4_and_subnet_mask_33.toml              |  18 ++
 .../lvm_maxroot_greater_than_maximum.json     |   3 +
 .../lvm_maxroot_greater_than_maximum.toml     |  16 ++
 .../lvm_swapsize_greater_than_hdsize.json     |   3 +
 .../lvm_swapsize_greater_than_hdsize.toml     |  16 ++
 .../zfs_raid_single_disk.json                 |   3 +
 .../zfs_raid_single_disk.toml                 |  15 ++
 proxmox-installer-common/src/disk_checks.rs   | 174 +++++++-----------
 proxmox-installer-common/src/options.rs       |  91 +++++++++
 proxmox-installer-common/src/utils.rs         |  84 +++++++--
 proxmox-tui-installer/src/views/bootdisk.rs   |  13 +-
 25 files changed, 482 insertions(+), 137 deletions(-)
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer/ipv4_and_subnet_31.json
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer/ipv4_and_subnet_31.toml
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/btrfs_raid_single_disk.json
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/btrfs_raid_single_disk.toml
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/duplicate_disk.json
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/duplicate_disk.toml
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_addr_is_network.json
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_addr_is_network.toml
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_mask_33.json
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_mask_33.toml
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_maxroot_greater_than_maximum.json
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_maxroot_greater_than_maximum.toml
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_swapsize_greater_than_hdsize.json
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_swapsize_greater_than_hdsize.toml
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/zfs_raid_single_disk.json
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/zfs_raid_single_disk.toml


Summary over all repositories:
  25 files changed, 482 insertions(+), 137 deletions(-)

-- 
Generated by git-murpp 0.8.0


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [pve-devel] [PATCH pve-installer v2 1/6] auto: add early answer file sanity check for RAID configurations
  2025-04-29 14:09 [pve-devel] [PATCH installer v2 0/6] add early disk and network sanity checks Michael Köppl
@ 2025-04-29 14:09 ` Michael Köppl
  2025-04-29 14:09 ` [pve-devel] [PATCH pve-installer v2 2/6] move RAID setup checks to RAID level enum implementations Michael Köppl
                   ` (4 subsequent siblings)
  5 siblings, 0 replies; 11+ messages in thread
From: Michael Köppl @ 2025-04-29 14:09 UTC (permalink / raw)
  To: pve-devel

The GUI and TUI installers already implement checks to ensure systems
have the minimum required number of disks available for the various RAID
configurations (min 2 disks for RAID1, min 4 disks for RAID10, etc).
This change adds an early check of the answer file to the
auto-installer, improving the user experience by avoiding failure during
the actual installation.

Signed-off-by: Michael Köppl <m.koeppl@proxmox.com>
---
 proxmox-auto-install-assistant/src/main.rs    |  7 ++--
 proxmox-auto-installer/src/utils.rs           | 18 ++++++++++-
 proxmox-auto-installer/tests/parse-answer.rs  |  4 ++-
 .../btrfs_raid_single_disk.json               |  3 ++
 .../btrfs_raid_single_disk.toml               | 15 +++++++++
 .../zfs_raid_single_disk.json                 |  3 ++
 .../zfs_raid_single_disk.toml                 | 15 +++++++++
 proxmox-installer-common/src/options.rs       | 32 +++++++++++++++++++
 8 files changed, 92 insertions(+), 5 deletions(-)
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/btrfs_raid_single_disk.json
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/btrfs_raid_single_disk.toml
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/zfs_raid_single_disk.json
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/zfs_raid_single_disk.toml

diff --git a/proxmox-auto-install-assistant/src/main.rs b/proxmox-auto-install-assistant/src/main.rs
index b64623b..3cf3abc 100644
--- a/proxmox-auto-install-assistant/src/main.rs
+++ b/proxmox-auto-install-assistant/src/main.rs
@@ -14,9 +14,9 @@ use proxmox_auto_installer::{
     answer::{Answer, FilterMatch},
     sysinfo::SysInfo,
     utils::{
-        AutoInstSettings, FetchAnswerFrom, HttpOptions, get_matched_udev_indexes, get_nic_list,
-        get_single_udev_index, verify_email_and_root_password_settings, verify_first_boot_settings,
-        verify_locale_settings,
+        get_matched_udev_indexes, get_nic_list, get_single_udev_index, verify_disks_settings,
+        verify_email_and_root_password_settings, verify_first_boot_settings,
+        verify_locale_settings, AutoInstSettings, FetchAnswerFrom, HttpOptions,
     },
 };
 use proxmox_installer_common::{FIRST_BOOT_EXEC_MAX_SIZE, FIRST_BOOT_EXEC_NAME};
@@ -597,6 +597,7 @@ fn parse_answer(path: impl AsRef<Path> + fmt::Debug) -> Result<Answer> {
     match toml::from_str(&contents) {
         Ok(answer) => {
             verify_locale_settings(&answer, &serde_json::from_str(LOCALE_INFO)?)?;
+            verify_disks_settings(&answer)?;
             verify_first_boot_settings(&answer)?;
             verify_email_and_root_password_settings(&answer)?;
             println!("The answer file was parsed successfully, no errors found!");
diff --git a/proxmox-auto-installer/src/utils.rs b/proxmox-auto-installer/src/utils.rs
index 365e01a..d6bc6e3 100644
--- a/proxmox-auto-installer/src/utils.rs
+++ b/proxmox-auto-installer/src/utils.rs
@@ -6,7 +6,8 @@ use std::{collections::BTreeMap, process::Command};
 
 use crate::{
     answer::{
-        self, Answer, FirstBootHookSourceMode, FqdnConfig, FqdnExtendedConfig, FqdnSourceMode,
+        self, Answer, DiskSelection, FirstBootHookSourceMode, FqdnConfig, FqdnExtendedConfig,
+        FqdnSourceMode,
     },
     udevinfo::UdevInfo,
 };
@@ -384,6 +385,20 @@ pub fn verify_email_and_root_password_settings(answer: &Answer) -> Result<()> {
     }
 }
 
+pub fn verify_disks_settings(answer: &Answer) -> Result<()> {
+    if let DiskSelection::Selection(selection) = &answer.disks.disk_selection {
+        let min_disks = answer.disks.fs_type.get_min_disks();
+        if selection.len() < min_disks {
+            bail!(
+                "{} requires at least {} disks",
+                answer.disks.fs_type,
+                min_disks
+            );
+        }
+    }
+    Ok(())
+}
+
 pub fn verify_first_boot_settings(answer: &Answer) -> Result<()> {
     info!("Verifying first boot settings");
 
@@ -414,6 +429,7 @@ pub fn parse_answer(
     let network_settings = get_network_settings(answer, udev_info, runtime_info, setup_info)?;
 
     verify_locale_settings(answer, locales)?;
+    verify_disks_settings(answer)?;
     verify_email_and_root_password_settings(answer)?;
     verify_first_boot_settings(answer)?;
 
diff --git a/proxmox-auto-installer/tests/parse-answer.rs b/proxmox-auto-installer/tests/parse-answer.rs
index 34bc969..92dba63 100644
--- a/proxmox-auto-installer/tests/parse-answer.rs
+++ b/proxmox-auto-installer/tests/parse-answer.rs
@@ -142,11 +142,13 @@ mod tests {
             run_named_fail_parse_test,
             // Keep below entries alphabetically sorted
             both_password_and_hashed_set,
+            btrfs_raid_single_disk,
             fqdn_from_dhcp_no_default_domain,
             fqdn_hostname_only,
             no_fqdn_from_dhcp,
             no_root_password_set,
-            short_password
+            short_password,
+            zfs_raid_single_disk
         );
     }
 }
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/btrfs_raid_single_disk.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/btrfs_raid_single_disk.json
new file mode 100644
index 0000000..37f35fe
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/btrfs_raid_single_disk.json
@@ -0,0 +1,3 @@
+{
+  "error": "BTRFS (RAID1) requires at least 2 disks"
+}
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/btrfs_raid_single_disk.toml b/proxmox-auto-installer/tests/resources/parse_answer_fail/btrfs_raid_single_disk.toml
new file mode 100644
index 0000000..e96fb16
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/btrfs_raid_single_disk.toml
@@ -0,0 +1,15 @@
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "btrfs-raid-single-disk.fail.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root-password = "12345678"
+
+[network]
+source = "from-dhcp"
+
+[disk-setup]
+filesystem = "btrfs"
+btrfs.raid = "raid1"
+disk-list = ["sda"]
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/zfs_raid_single_disk.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/zfs_raid_single_disk.json
new file mode 100644
index 0000000..9a8cc90
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/zfs_raid_single_disk.json
@@ -0,0 +1,3 @@
+{
+  "error": "ZFS (RAID10) requires at least 4 disks"
+}
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/zfs_raid_single_disk.toml b/proxmox-auto-installer/tests/resources/parse_answer_fail/zfs_raid_single_disk.toml
new file mode 100644
index 0000000..94ae748
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/zfs_raid_single_disk.toml
@@ -0,0 +1,15 @@
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "zfs-raid-single-disk.fail.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root-password = "12345678"
+
+[network]
+source = "from-dhcp"
+
+[disk-setup]
+filesystem = "zfs"
+zfs.raid = "raid10"
+disk-list = ["sda"]
diff --git a/proxmox-installer-common/src/options.rs b/proxmox-installer-common/src/options.rs
index 9cc4ee0..9271b8b 100644
--- a/proxmox-installer-common/src/options.rs
+++ b/proxmox-installer-common/src/options.rs
@@ -20,6 +20,16 @@ pub enum BtrfsRaidLevel {
     Raid10,
 }
 
+impl BtrfsRaidLevel {
+    pub fn get_min_disks(&self) -> usize {
+        match self {
+            BtrfsRaidLevel::Raid0 => 1,
+            BtrfsRaidLevel::Raid1 => 2,
+            BtrfsRaidLevel::Raid10 => 4,
+        }
+    }
+}
+
 serde_plain::derive_display_from_serialize!(BtrfsRaidLevel);
 
 #[derive(Copy, Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
@@ -48,6 +58,19 @@ pub enum ZfsRaidLevel {
     RaidZ3,
 }
 
+impl ZfsRaidLevel {
+    pub fn get_min_disks(&self) -> usize {
+        match self {
+            ZfsRaidLevel::Raid0 => 1,
+            ZfsRaidLevel::Raid1 => 2,
+            ZfsRaidLevel::Raid10 => 4,
+            ZfsRaidLevel::RaidZ => 3,
+            ZfsRaidLevel::RaidZ2 => 4,
+            ZfsRaidLevel::RaidZ3 => 5,
+        }
+    }
+}
+
 serde_plain::derive_display_from_serialize!(ZfsRaidLevel);
 
 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
@@ -67,6 +90,15 @@ impl FsType {
     pub fn is_lvm(&self) -> bool {
         matches!(self, FsType::Ext4 | FsType::Xfs)
     }
+
+    pub fn get_min_disks(&self) -> usize {
+        match self {
+            FsType::Ext4 => 1,
+            FsType::Xfs => 1,
+            FsType::Zfs(level) => level.get_min_disks(),
+            FsType::Btrfs(level) => level.get_min_disks(),
+        }
+    }
 }
 
 impl fmt::Display for FsType {
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [pve-devel] [PATCH pve-installer v2 2/6] move RAID setup checks to RAID level enum implementations
  2025-04-29 14:09 [pve-devel] [PATCH installer v2 0/6] add early disk and network sanity checks Michael Köppl
  2025-04-29 14:09 ` [pve-devel] [PATCH pve-installer v2 1/6] auto: add early answer file sanity check for RAID configurations Michael Köppl
@ 2025-04-29 14:09 ` Michael Köppl
  2025-04-29 14:09 ` [pve-devel] [PATCH pve-installer v2 3/6] close #5887: add sanity check for LVM swapsize and maxroot Michael Köppl
                   ` (3 subsequent siblings)
  5 siblings, 0 replies; 11+ messages in thread
From: Michael Köppl @ 2025-04-29 14:09 UTC (permalink / raw)
  To: pve-devel

Instead of having parts of the RAID setup checks scattered in multiple
places, move the core of the checks to implementations of the
ZfsRaidLevel and BtrfsRaidLevel enums.

Signed-off-by: Michael Köppl <m.koeppl@proxmox.com>
---
 proxmox-installer-common/src/disk_checks.rs | 156 ++++----------------
 proxmox-installer-common/src/options.rs     |  59 ++++++++
 proxmox-tui-installer/src/views/bootdisk.rs |  13 +-
 3 files changed, 96 insertions(+), 132 deletions(-)

diff --git a/proxmox-installer-common/src/disk_checks.rs b/proxmox-installer-common/src/disk_checks.rs
index ecc43bd..d535837 100644
--- a/proxmox-installer-common/src/disk_checks.rs
+++ b/proxmox-installer-common/src/disk_checks.rs
@@ -1,6 +1,6 @@
 use std::collections::HashSet;
 
-use crate::options::{BtrfsRaidLevel, Disk, ZfsRaidLevel};
+use crate::options::Disk;
 use crate::setup::BootType;
 
 /// Checks a list of disks for duplicate entries, using their index as key.
@@ -49,94 +49,10 @@ pub fn check_disks_4kn_legacy_boot(boot_type: BootType, disks: &[Disk]) -> Resul
     Ok(())
 }
 
-/// Checks whether a user-supplied ZFS RAID setup is valid or not, such as disk sizes andminimum
-/// number of disks.
-///
-/// # Arguments
-///
-/// * `level` - The targeted ZFS RAID level by the user.
-/// * `disks` - List of disks designated as RAID targets.
-pub fn check_zfs_raid_config(level: ZfsRaidLevel, disks: &[Disk]) -> Result<(), String> {
-    // See also Proxmox/Install.pm:get_zfs_raid_setup()
-
-    let check_mirror_size = |disk1: &Disk, disk2: &Disk| {
-        if (disk1.size - disk2.size).abs() > disk1.size / 10. {
-            Err(format!(
-                "Mirrored disks must have same size:\n\n  * {disk1}\n  * {disk2}"
-            ))
-        } else {
-            Ok(())
-        }
-    };
-
-    match level {
-        ZfsRaidLevel::Raid0 => check_raid_min_disks(disks, 1)?,
-        ZfsRaidLevel::Raid1 => {
-            check_raid_min_disks(disks, 2)?;
-            for disk in disks {
-                check_mirror_size(&disks[0], disk)?;
-            }
-        }
-        ZfsRaidLevel::Raid10 => {
-            check_raid_min_disks(disks, 4)?;
-
-            if disks.len() % 2 != 0 {
-                return Err(format!(
-                    "Needs an even number of disks, currently selected: {}",
-                    disks.len(),
-                ));
-            }
-
-            // Pairs need to have the same size
-            for i in (0..disks.len()).step_by(2) {
-                check_mirror_size(&disks[i], &disks[i + 1])?;
-            }
-        }
-        // For RAID-Z: minimum disks number is level + 2
-        ZfsRaidLevel::RaidZ => {
-            check_raid_min_disks(disks, 3)?;
-            for disk in disks {
-                check_mirror_size(&disks[0], disk)?;
-            }
-        }
-        ZfsRaidLevel::RaidZ2 => {
-            check_raid_min_disks(disks, 4)?;
-            for disk in disks {
-                check_mirror_size(&disks[0], disk)?;
-            }
-        }
-        ZfsRaidLevel::RaidZ3 => {
-            check_raid_min_disks(disks, 5)?;
-            for disk in disks {
-                check_mirror_size(&disks[0], disk)?;
-            }
-        }
-    }
-
-    Ok(())
-}
-
-/// Checks whether a user-supplied Btrfs RAID setup is valid or not, such as minimum
-/// number of disks.
-///
-/// # Arguments
-///
-/// * `level` - The targeted Btrfs RAID level by the user.
-/// * `disks` - List of disks designated as RAID targets.
-pub fn check_btrfs_raid_config(level: BtrfsRaidLevel, disks: &[Disk]) -> Result<(), String> {
-    // See also Proxmox/Install.pm:get_btrfs_raid_setup()
-
-    match level {
-        BtrfsRaidLevel::Raid0 => check_raid_min_disks(disks, 1)?,
-        BtrfsRaidLevel::Raid1 => check_raid_min_disks(disks, 2)?,
-        BtrfsRaidLevel::Raid10 => check_raid_min_disks(disks, 4)?,
-    }
-
-    Ok(())
-}
-
 #[cfg(test)]
 mod tests {
+    use crate::options::{BtrfsRaidLevel, ZfsRaidLevel};
+
     use super::*;
 
     fn dummy_disk(index: usize) -> Disk {
@@ -194,50 +110,38 @@ mod tests {
     fn btrfs_raid() {
         let disks = dummy_disks(10);
 
-        assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid0, &[]).is_err());
-        assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid0, &disks[..1]).is_ok());
-        assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid0, &disks).is_ok());
-
-        assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid1, &[]).is_err());
-        assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid1, &disks[..1]).is_err());
-        assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid1, &disks[..2]).is_ok());
-        assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid1, &disks).is_ok());
-
-        assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid10, &[]).is_err());
-        assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid10, &disks[..3]).is_err());
-        assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid10, &disks[..4]).is_ok());
-        assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid10, &disks).is_ok());
+        let btrfs_raid_variants = [
+            BtrfsRaidLevel::Raid0,
+            BtrfsRaidLevel::Raid1,
+            BtrfsRaidLevel::Raid10,
+        ];
+
+        for v in btrfs_raid_variants {
+            assert!(v.check_disks(&[]).is_err());
+            assert!(v.check_disks(&disks[..v.get_min_disks() - 1]).is_err());
+            assert!(v.check_disks(&disks[..v.get_min_disks()]).is_ok());
+            assert!(v.check_disks(&disks).is_ok());
+        }
     }
 
     #[test]
     fn zfs_raid() {
         let disks = dummy_disks(10);
 
-        assert!(check_zfs_raid_config(ZfsRaidLevel::Raid0, &[]).is_err());
-        assert!(check_zfs_raid_config(ZfsRaidLevel::Raid0, &disks[..1]).is_ok());
-        assert!(check_zfs_raid_config(ZfsRaidLevel::Raid0, &disks).is_ok());
-
-        assert!(check_zfs_raid_config(ZfsRaidLevel::Raid1, &[]).is_err());
-        assert!(check_zfs_raid_config(ZfsRaidLevel::Raid1, &disks[..2]).is_ok());
-        assert!(check_zfs_raid_config(ZfsRaidLevel::Raid1, &disks).is_ok());
-
-        assert!(check_zfs_raid_config(ZfsRaidLevel::Raid10, &[]).is_err());
-        assert!(check_zfs_raid_config(ZfsRaidLevel::Raid10, &dummy_disks(4)).is_ok());
-        assert!(check_zfs_raid_config(ZfsRaidLevel::Raid10, &disks).is_ok());
-
-        assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ, &[]).is_err());
-        assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ, &disks[..2]).is_err());
-        assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ, &disks[..3]).is_ok());
-        assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ, &disks).is_ok());
-
-        assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ2, &[]).is_err());
-        assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ2, &disks[..3]).is_err());
-        assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ2, &disks[..4]).is_ok());
-        assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ2, &disks).is_ok());
-
-        assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ3, &[]).is_err());
-        assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ3, &disks[..4]).is_err());
-        assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ3, &disks[..5]).is_ok());
-        assert!(check_zfs_raid_config(ZfsRaidLevel::RaidZ3, &disks).is_ok());
+        let zfs_raid_variants = [
+            ZfsRaidLevel::Raid0,
+            ZfsRaidLevel::Raid1,
+            ZfsRaidLevel::Raid10,
+            ZfsRaidLevel::RaidZ,
+            ZfsRaidLevel::RaidZ2,
+            ZfsRaidLevel::RaidZ3,
+        ];
+
+        for v in zfs_raid_variants {
+            assert!(v.check_disks(&[]).is_err());
+            assert!(v.check_disks(&disks[..v.get_min_disks() - 1]).is_err());
+            assert!(v.check_disks(&disks[..v.get_min_disks()]).is_ok());
+            assert!(v.check_disks(&disks).is_ok());
+        }
     }
 }
diff --git a/proxmox-installer-common/src/options.rs b/proxmox-installer-common/src/options.rs
index 9271b8b..0552954 100644
--- a/proxmox-installer-common/src/options.rs
+++ b/proxmox-installer-common/src/options.rs
@@ -6,6 +6,7 @@ use std::str::FromStr;
 use std::sync::OnceLock;
 use std::{cmp, fmt};
 
+use crate::disk_checks::check_raid_min_disks;
 use crate::setup::{LocaleInfo, NetworkInfo, RuntimeInfo, SetupInfo};
 use crate::utils::{CidrAddress, Fqdn};
 
@@ -28,6 +29,17 @@ impl BtrfsRaidLevel {
             BtrfsRaidLevel::Raid10 => 4,
         }
     }
+
+    /// Checks whether a user-supplied Btrfs RAID setup is valid or not, such as minimum
+    /// number of disks.
+    ///
+    /// # Arguments
+    ///
+    /// * `disks` - List of disks designated as RAID targets.
+    pub fn check_disks(&self, disks: &[Disk]) -> Result<(), String> {
+        check_raid_min_disks(disks, self.get_min_disks())?;
+        Ok(())
+    }
 }
 
 serde_plain::derive_display_from_serialize!(BtrfsRaidLevel);
@@ -69,6 +81,53 @@ impl ZfsRaidLevel {
             ZfsRaidLevel::RaidZ3 => 5,
         }
     }
+
+    fn check_mirror_size(&self, disk1: &Disk, disk2: &Disk) -> Result<(), String> {
+        if (disk1.size - disk2.size).abs() > disk1.size / 10. {
+            Err(format!(
+                "Mirrored disks must have same size:\n\n  * {disk1}\n  * {disk2}"
+            ))
+        } else {
+            Ok(())
+        }
+    }
+
+    /// Checks whether a user-supplied ZFS RAID setup is valid or not, such as disk sizes andminimum
+    /// number of disks.
+    ///
+    /// # Arguments
+    ///
+    /// * `disks` - List of disks designated as RAID targets.
+    pub fn check_disks(&self, disks: &[Disk]) -> Result<(), String> {
+        check_raid_min_disks(disks, self.get_min_disks())?;
+
+        match self {
+            ZfsRaidLevel::Raid0 => {}
+            ZfsRaidLevel::Raid10 => {
+                if disks.len() % 2 != 0 {
+                    return Err(format!(
+                        "Needs an even number of disks, currently selected: {}",
+                        disks.len(),
+                    ));
+                }
+
+                // Pairs need to have the same size
+                for i in (0..disks.len()).step_by(2) {
+                    self.check_mirror_size(&disks[i], &disks[i + 1])?;
+                }
+            }
+            ZfsRaidLevel::Raid1
+            | ZfsRaidLevel::RaidZ
+            | ZfsRaidLevel::RaidZ2
+            | ZfsRaidLevel::RaidZ3 => {
+                for disk in disks {
+                    self.check_mirror_size(&disks[0], disk)?;
+                }
+            }
+        }
+
+        Ok(())
+    }
 }
 
 serde_plain::derive_display_from_serialize!(ZfsRaidLevel);
diff --git a/proxmox-tui-installer/src/views/bootdisk.rs b/proxmox-tui-installer/src/views/bootdisk.rs
index 313a3c9..e87b040 100644
--- a/proxmox-tui-installer/src/views/bootdisk.rs
+++ b/proxmox-tui-installer/src/views/bootdisk.rs
@@ -17,10 +17,7 @@ use crate::InstallerState;
 use crate::options::FS_TYPES;
 
 use proxmox_installer_common::{
-    disk_checks::{
-        check_btrfs_raid_config, check_disks_4kn_legacy_boot, check_for_duplicate_disks,
-        check_zfs_raid_config,
-    },
+    disk_checks::{check_disks_4kn_legacy_boot, check_for_duplicate_disks},
     options::{
         AdvancedBootdiskOptions, BTRFS_COMPRESS_OPTIONS, BootdiskOptions, BtrfsBootdiskOptions,
         Disk, FsType, LvmBootdiskOptions, ZFS_CHECKSUM_OPTIONS, ZFS_COMPRESS_OPTIONS,
@@ -275,7 +272,9 @@ impl AdvancedBootdiskOptionsView {
                 .ok_or("Failed to retrieve advanced bootdisk options")?;
 
             if let FsType::Zfs(level) = fstype {
-                check_zfs_raid_config(level, &disks).map_err(|err| format!("{fstype}: {err}"))?;
+                level
+                    .check_disks(&disks)
+                    .map_err(|err| format!("{fstype}: {err}"))?;
             }
 
             Ok(BootdiskOptions {
@@ -289,7 +288,9 @@ impl AdvancedBootdiskOptionsView {
                 .ok_or("Failed to retrieve advanced bootdisk options")?;
 
             if let FsType::Btrfs(level) = fstype {
-                check_btrfs_raid_config(level, &disks).map_err(|err| format!("{fstype}: {err}"))?;
+                level
+                    .check_disks(&disks)
+                    .map_err(|err| format!("{fstype}: {err}"))?;
             }
 
             Ok(BootdiskOptions {
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [pve-devel] [PATCH pve-installer v2 3/6] close #5887: add sanity check for LVM swapsize and maxroot
  2025-04-29 14:09 [pve-devel] [PATCH installer v2 0/6] add early disk and network sanity checks Michael Köppl
  2025-04-29 14:09 ` [pve-devel] [PATCH pve-installer v2 1/6] auto: add early answer file sanity check for RAID configurations Michael Köppl
  2025-04-29 14:09 ` [pve-devel] [PATCH pve-installer v2 2/6] move RAID setup checks to RAID level enum implementations Michael Köppl
@ 2025-04-29 14:09 ` Michael Köppl
  2025-05-06 11:48   ` Christoph Heiss
  2025-04-29 14:09 ` [pve-devel] [PATCH pve-installer v2 4/6] auto: add check for duplicate disks in answer file Michael Köppl
                   ` (2 subsequent siblings)
  5 siblings, 1 reply; 11+ messages in thread
From: Michael Köppl @ 2025-04-29 14:09 UTC (permalink / raw)
  To: pve-devel

Check that the configured swapsize is not greater than hdsize / 8 as
stated in the admin guide [0]. Additionally check that maxroot is at
most hdsize / 4. Define the behavior for the auto-installer as well as
the TUI and GUI installers.

[0] https://pve.proxmox.com/pve-docs/pve-admin-guide.html#advanced_lvm_options

Signed-off-by: Michael Köppl <m.koeppl@proxmox.com>
---
Note regarding the change around set_hdsize in proxinstall: Before this
change, the hdsize was only set if the user manually changed it. If the
user did not change it, any checks against hdsize would check against
undefined.

 Proxmox/Install.pm                            | 16 ++++++
 proxinstall                                   |  5 +-
 proxmox-auto-installer/src/utils.rs           | 16 ++++++
 proxmox-auto-installer/tests/parse-answer.rs  |  4 +-
 .../lvm_maxroot_greater_than_maximum.json     |  3 ++
 .../lvm_maxroot_greater_than_maximum.toml     | 16 ++++++
 .../lvm_swapsize_greater_than_hdsize.json     |  3 ++
 .../lvm_swapsize_greater_than_hdsize.toml     | 16 ++++++
 proxmox-installer-common/src/disk_checks.rs   | 52 ++++++++++++++++++-
 proxmox-tui-installer/src/views/bootdisk.rs   |  6 ++-
 10 files changed, 133 insertions(+), 4 deletions(-)
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_maxroot_greater_than_maximum.json
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_maxroot_greater_than_maximum.toml
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_swapsize_greater_than_hdsize.json
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_swapsize_greater_than_hdsize.toml

diff --git a/Proxmox/Install.pm b/Proxmox/Install.pm
index f673604..7c55d1f 100644
--- a/Proxmox/Install.pm
+++ b/Proxmox/Install.pm
@@ -592,6 +592,22 @@ sub compute_swapsize {
     return $swapsize_kb;
 }
 
+sub swapsize_check {
+    my ($hdsize) = @_;
+    my $swapsize = Proxmox::Install::Config::get_swapsize();
+    my $threshold = $hdsize / 8;
+    die "swap size ${swapsize} GiB cannot be greater than ${threshold} GiB (hard disk size / 8)\n"
+	if $swapsize > $threshold;
+}
+
+sub maxroot_size_check {
+    my ($hdsize) = @_;
+    my $maxroot = Proxmox::Install::Config::get_maxroot();
+    my $threshold = $hdsize / 4;
+    die "maximum root volume size ${maxroot} GiB cannot be greater than ${threshold} GiB (hard disk size / 4)\n"
+	if $maxroot > $threshold;
+}
+
 my sub chroot_chown {
     my ($root, $path, %param) = @_;
 
diff --git a/proxinstall b/proxinstall
index bc9ade6..e9ff6e8 100755
--- a/proxinstall
+++ b/proxinstall
@@ -1406,7 +1406,7 @@ sub create_hdoption_view {
 
     my $tmp;
 
-    if (($tmp = &$get_float($spinbutton_hdsize)) && ($tmp != $hdsize)) {
+    if (defined($tmp = &$get_float($spinbutton_hdsize))) {
 	Proxmox::Install::Config::set_hdsize($tmp);
     } else {
 	Proxmox::Install::Config::set_hdsize(undef);
@@ -1521,9 +1521,12 @@ sub create_hdsel_view {
 	    $target_hds = [ map { $_->[1] } @$devlist ];
 	} else {
 	    my $target_hd = Proxmox::Install::Config::get_target_hd();
+	    my $hdsize = Proxmox::Install::Config::get_hdsize();
 	    eval {
 		my $target_block_size = Proxmox::Sys::Block::logical_blocksize($target_hd);
 		Proxmox::Install::legacy_bios_4k_check($target_block_size);
+		Proxmox::Install::swapsize_check($hdsize);
+		Proxmox::Install::maxroot_size_check($hdsize);
 	    };
 	    if (my $err = $@) {
 		Proxmox::UI::message("Warning: $err\n");
diff --git a/proxmox-auto-installer/src/utils.rs b/proxmox-auto-installer/src/utils.rs
index d6bc6e3..75696bd 100644
--- a/proxmox-auto-installer/src/utils.rs
+++ b/proxmox-auto-installer/src/utils.rs
@@ -13,6 +13,7 @@ use crate::{
 };
 use proxmox_installer_common::{
     ROOT_PASSWORD_MIN_LENGTH,
+    disk_checks::{check_maxroot, check_swapsize},
     options::{FsType, NetworkOptions, ZfsChecksumOption, ZfsCompressOption, email_validate},
     setup::{
         InstallBtrfsOption, InstallConfig, InstallFirstBootSetup, InstallRootPassword,
@@ -396,6 +397,21 @@ pub fn verify_disks_settings(answer: &Answer) -> Result<()> {
             );
         }
     }
+
+    if let answer::FsOptions::LVM(lvm) = &answer.disks.fs_options {
+        if let Some((swapsize, hdsize)) = lvm.swapsize.zip(lvm.hdsize) {
+            if let Err(err) = check_swapsize(swapsize, hdsize) {
+                bail!(err);
+            }
+        }
+
+        if let Some((maxroot, hdsize)) = lvm.maxroot.zip(lvm.hdsize) {
+            if let Err(err) = check_maxroot(maxroot, hdsize) {
+                bail!(err);
+            }
+        }
+    }
+
     Ok(())
 }
 
diff --git a/proxmox-auto-installer/tests/parse-answer.rs b/proxmox-auto-installer/tests/parse-answer.rs
index 92dba63..e615672 100644
--- a/proxmox-auto-installer/tests/parse-answer.rs
+++ b/proxmox-auto-installer/tests/parse-answer.rs
@@ -7,7 +7,7 @@ use proxmox_auto_installer::udevinfo::UdevInfo;
 use proxmox_auto_installer::utils::parse_answer;
 
 use proxmox_installer_common::setup::{
-    LocaleInfo, RuntimeInfo, SetupInfo, load_installer_setup_files, read_json,
+    load_installer_setup_files, read_json, LocaleInfo, RuntimeInfo, SetupInfo,
 };
 
 fn get_test_resource_path() -> Result<PathBuf, String> {
@@ -145,6 +145,8 @@ mod tests {
             btrfs_raid_single_disk,
             fqdn_from_dhcp_no_default_domain,
             fqdn_hostname_only,
+            lvm_maxroot_greater_than_maximum,
+            lvm_swapsize_greater_than_hdsize,
             no_fqdn_from_dhcp,
             no_root_password_set,
             short_password,
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_maxroot_greater_than_maximum.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_maxroot_greater_than_maximum.json
new file mode 100644
index 0000000..bab12e6
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_maxroot_greater_than_maximum.json
@@ -0,0 +1,3 @@
+{
+  "error": "Maximum root volume size 8.01 GiB cannot be greater than 8 GiB (hard disk size / 4)"
+}
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_maxroot_greater_than_maximum.toml b/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_maxroot_greater_than_maximum.toml
new file mode 100644
index 0000000..e934d29
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_maxroot_greater_than_maximum.toml
@@ -0,0 +1,16 @@
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "btrfs-raid-single-disk.fail.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root-password = "12345678"
+
+[network]
+source = "from-dhcp"
+
+[disk-setup]
+filesystem = "ext4"
+lvm.maxroot = 8.01
+lvm.hdsize = 32
+disk-list = ["sda"]
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_swapsize_greater_than_hdsize.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_swapsize_greater_than_hdsize.json
new file mode 100644
index 0000000..aa4f7fe
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_swapsize_greater_than_hdsize.json
@@ -0,0 +1,3 @@
+{
+  "error": "Swap size 4.01 GiB cannot be greater than 4 GiB (hard disk size / 8)"
+}
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_swapsize_greater_than_hdsize.toml b/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_swapsize_greater_than_hdsize.toml
new file mode 100644
index 0000000..ffe16dc
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_swapsize_greater_than_hdsize.toml
@@ -0,0 +1,16 @@
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "btrfs-raid-single-disk.fail.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root-password = "12345678"
+
+[network]
+source = "from-dhcp"
+
+[disk-setup]
+filesystem = "ext4"
+lvm.swapsize = 4.01
+lvm.hdsize = 32
+disk-list = ["sda"]
diff --git a/proxmox-installer-common/src/disk_checks.rs b/proxmox-installer-common/src/disk_checks.rs
index d535837..77104ae 100644
--- a/proxmox-installer-common/src/disk_checks.rs
+++ b/proxmox-installer-common/src/disk_checks.rs
@@ -1,6 +1,6 @@
 use std::collections::HashSet;
 
-use crate::options::Disk;
+use crate::options::{Disk, LvmBootdiskOptions};
 use crate::setup::BootType;
 
 /// Checks a list of disks for duplicate entries, using their index as key.
@@ -49,6 +49,56 @@ pub fn check_disks_4kn_legacy_boot(boot_type: BootType, disks: &[Disk]) -> Resul
     Ok(())
 }
 
+/// Checks whether the configured swap size exceeds the allowed threshold.
+///
+/// # Arguments
+///
+/// * `swapsize` - The size of the swap in GiB
+/// * `hdsize` - The total size of the hard disk in GiB
+pub fn check_swapsize(swapsize: f64, hdsize: f64) -> Result<(), String> {
+    let threshold = hdsize / 8.0;
+    if swapsize > threshold {
+        return Err(format!(
+            "Swap size {swapsize} GiB cannot be greater than {threshold} GiB (hard disk size / 8)",
+        ));
+    }
+    Ok(())
+}
+
+/// Checks whether the configured root volume size exceeds the allowed threshold.
+///
+/// # Arguments
+///
+/// * `maxroot` - The size of the root volume in GiB
+/// * `hdsize` - The total size of the hard disk in GiB
+pub fn check_maxroot(maxroot: f64, hdsize: f64) -> Result<(), String> {
+    let threshold = hdsize / 4.0;
+    if maxroot > threshold {
+        return Err(format!(
+            "Maximum root volume size {maxroot} GiB cannot be greater than {threshold} GiB (hard disk size / 4)",
+        ));
+    }
+    Ok(())
+}
+
+/// Checks whether a user-supplied LVM setup is valid or not, such as the swapsize or maxroot not
+/// exceeding certain thresholds.
+///
+/// # Arguments
+///
+/// * `bootdisk_opts` - The LVM options set by the user.
+pub fn check_lvm_bootdisk_opts(bootdisk_opts: &LvmBootdiskOptions) -> Result<(), String> {
+    if let Some(swap_size) = bootdisk_opts.swap_size {
+        check_swapsize(swap_size, bootdisk_opts.total_size)?;
+    }
+
+    if let Some(max_root_size) = bootdisk_opts.max_root_size {
+        check_maxroot(max_root_size, bootdisk_opts.total_size)?;
+    }
+
+    Ok(())
+}
+
 #[cfg(test)]
 mod tests {
     use crate::options::{BtrfsRaidLevel, ZfsRaidLevel};
diff --git a/proxmox-tui-installer/src/views/bootdisk.rs b/proxmox-tui-installer/src/views/bootdisk.rs
index e87b040..b94cf38 100644
--- a/proxmox-tui-installer/src/views/bootdisk.rs
+++ b/proxmox-tui-installer/src/views/bootdisk.rs
@@ -17,7 +17,9 @@ use crate::InstallerState;
 use crate::options::FS_TYPES;
 
 use proxmox_installer_common::{
-    disk_checks::{check_disks_4kn_legacy_boot, check_for_duplicate_disks},
+    disk_checks::{
+        check_disks_4kn_legacy_boot, check_for_duplicate_disks, check_lvm_bootdisk_opts,
+    },
     options::{
         AdvancedBootdiskOptions, BTRFS_COMPRESS_OPTIONS, BootdiskOptions, BtrfsBootdiskOptions,
         Disk, FsType, LvmBootdiskOptions, ZFS_CHECKSUM_OPTIONS, ZFS_COMPRESS_OPTIONS,
@@ -261,6 +263,8 @@ impl AdvancedBootdiskOptionsView {
                 .get_values()
                 .ok_or("Failed to retrieve advanced bootdisk options")?;
 
+            check_lvm_bootdisk_opts(&advanced).map_err(|err| format!("{fstype}: {err}"))?;
+
             Ok(BootdiskOptions {
                 disks: vec![disk],
                 fstype,
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [pve-devel] [PATCH pve-installer v2 4/6] auto: add check for duplicate disks in answer file
  2025-04-29 14:09 [pve-devel] [PATCH installer v2 0/6] add early disk and network sanity checks Michael Köppl
                   ` (2 preceding siblings ...)
  2025-04-29 14:09 ` [pve-devel] [PATCH pve-installer v2 3/6] close #5887: add sanity check for LVM swapsize and maxroot Michael Köppl
@ 2025-04-29 14:09 ` Michael Köppl
  2025-04-29 14:09 ` [pve-devel] [PATCH pve-installer v2 5/6] common: add more descriptive errors for invalid network configs Michael Köppl
  2025-04-29 14:09 ` [pve-devel] [RFC PATCH pve-installer v2 6/6] common: add checks for valid subnet mask and IPv4 address within subnet Michael Köppl
  5 siblings, 0 replies; 11+ messages in thread
From: Michael Köppl @ 2025-04-29 14:09 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Michael Köppl <m.koeppl@proxmox.com>
---
 proxmox-auto-installer/src/utils.rs               | 12 +++++++++++-
 proxmox-auto-installer/tests/parse-answer.rs      |  1 +
 .../parse_answer_fail/duplicate_disk.json         |  3 +++
 .../parse_answer_fail/duplicate_disk.toml         | 15 +++++++++++++++
 4 files changed, 30 insertions(+), 1 deletion(-)
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/duplicate_disk.json
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/duplicate_disk.toml

diff --git a/proxmox-auto-installer/src/utils.rs b/proxmox-auto-installer/src/utils.rs
index 75696bd..3a1573e 100644
--- a/proxmox-auto-installer/src/utils.rs
+++ b/proxmox-auto-installer/src/utils.rs
@@ -2,7 +2,10 @@ use anyhow::{Context, Result, bail};
 use clap::ValueEnum;
 use glob::Pattern;
 use log::info;
-use std::{collections::BTreeMap, process::Command};
+use std::{
+    collections::{BTreeMap, HashSet},
+    process::Command,
+};
 
 use crate::{
     answer::{
@@ -396,6 +399,13 @@ pub fn verify_disks_settings(answer: &Answer) -> Result<()> {
                 min_disks
             );
         }
+
+        let mut disk_set = HashSet::new();
+        for disk in selection {
+            if !disk_set.insert(disk) {
+                bail!("List of disks contains duplicate disk {disk}");
+            }
+        }
     }
 
     if let answer::FsOptions::LVM(lvm) = &answer.disks.fs_options {
diff --git a/proxmox-auto-installer/tests/parse-answer.rs b/proxmox-auto-installer/tests/parse-answer.rs
index e615672..f560483 100644
--- a/proxmox-auto-installer/tests/parse-answer.rs
+++ b/proxmox-auto-installer/tests/parse-answer.rs
@@ -143,6 +143,7 @@ mod tests {
             // Keep below entries alphabetically sorted
             both_password_and_hashed_set,
             btrfs_raid_single_disk,
+            duplicate_disk,
             fqdn_from_dhcp_no_default_domain,
             fqdn_hostname_only,
             lvm_maxroot_greater_than_maximum,
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/duplicate_disk.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/duplicate_disk.json
new file mode 100644
index 0000000..d01fabe
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/duplicate_disk.json
@@ -0,0 +1,3 @@
+{
+  "error": "List of disks contains duplicate disk sda"
+}
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/duplicate_disk.toml b/proxmox-auto-installer/tests/resources/parse_answer_fail/duplicate_disk.toml
new file mode 100644
index 0000000..bff1631
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/duplicate_disk.toml
@@ -0,0 +1,15 @@
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "btrfs-raid-single-disk.fail.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root-password = "12345678"
+
+[network]
+source = "from-dhcp"
+
+[disk-setup]
+filesystem = "btrfs"
+btrfs.raid = "raid1"
+disk-list = ["sda", "sdb", "sda"]
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [pve-devel] [PATCH pve-installer v2 5/6] common: add more descriptive errors for invalid network configs
  2025-04-29 14:09 [pve-devel] [PATCH installer v2 0/6] add early disk and network sanity checks Michael Köppl
                   ` (3 preceding siblings ...)
  2025-04-29 14:09 ` [pve-devel] [PATCH pve-installer v2 4/6] auto: add check for duplicate disks in answer file Michael Köppl
@ 2025-04-29 14:09 ` Michael Köppl
  2025-04-29 14:09 ` [pve-devel] [RFC PATCH pve-installer v2 6/6] common: add checks for valid subnet mask and IPv4 address within subnet Michael Köppl
  5 siblings, 0 replies; 11+ messages in thread
From: Michael Köppl @ 2025-04-29 14:09 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Michael Köppl <m.koeppl@proxmox.com>
---
 proxmox-installer-common/src/utils.rs | 17 +++++++++++++++--
 1 file changed, 15 insertions(+), 2 deletions(-)

diff --git a/proxmox-installer-common/src/utils.rs b/proxmox-installer-common/src/utils.rs
index 8adcec0..1fe6a74 100644
--- a/proxmox-installer-common/src/utils.rs
+++ b/proxmox-installer-common/src/utils.rs
@@ -18,6 +18,20 @@ pub enum CidrAddressParseError {
     InvalidMask(Option<ParseIntError>),
 }
 
+impl fmt::Display for CidrAddressParseError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let msg = match &self {
+            CidrAddressParseError::NoDelimiter => {
+                String::from("No delimiter for separating address and mask was found")
+            }
+            CidrAddressParseError::InvalidAddr(addr_parse_error) => format!("{addr_parse_error}"),
+            CidrAddressParseError::InvalidMask(parse_int_error) => format!("{:?}", parse_int_error),
+        };
+
+        write!(f, "Invalid CIDR: {msg}")
+    }
+}
+
 /// An IP address (IPv4 or IPv6), including network mask.
 ///
 /// See the [`IpAddr`] type for more information how IP addresses are handled.
@@ -109,8 +123,7 @@ impl<'de> Deserialize<'de> for CidrAddress {
         D: serde::Deserializer<'de>,
     {
         let s: String = Deserialize::deserialize(deserializer)?;
-        s.parse()
-            .map_err(|_| serde::de::Error::custom("invalid CIDR"))
+        s.parse().map_err(serde::de::Error::custom)
     }
 }
 
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [pve-devel] [RFC PATCH pve-installer v2 6/6] common: add checks for valid subnet mask and IPv4 address within subnet
  2025-04-29 14:09 [pve-devel] [PATCH installer v2 0/6] add early disk and network sanity checks Michael Köppl
                   ` (4 preceding siblings ...)
  2025-04-29 14:09 ` [pve-devel] [PATCH pve-installer v2 5/6] common: add more descriptive errors for invalid network configs Michael Köppl
@ 2025-04-29 14:09 ` Michael Köppl
  2025-05-06  9:21   ` Christoph Heiss
  5 siblings, 1 reply; 11+ messages in thread
From: Michael Köppl @ 2025-04-29 14:09 UTC (permalink / raw)
  To: pve-devel

Add checks for valid subnet mask (greater than /0 and at most /32 for
IPv4). In addition, check if the address entered by the user is valid
within the given subnet, i.e. not a network address or broadcast
address. /31 is considered an exception in accordance with RFC3021 [0],
considering any of the 2 available addresses to be valid host addresses.

[0] https://datatracker.ietf.org/doc/html/rfc3021

Signed-off-by: Michael Köppl <m.koeppl@proxmox.com>
---
Some input / discussion would be much appreciated here, since this might
again be considered too restrictive.

 proxmox-auto-installer/tests/parse-answer.rs  |  3 +
 .../parse_answer/ipv4_and_subnet_31.json      | 19 +++++
 .../parse_answer/ipv4_and_subnet_31.toml      | 18 +++++
 .../ipv4_and_subnet_addr_is_network.json      |  3 +
 .../ipv4_and_subnet_addr_is_network.toml      | 18 +++++
 .../ipv4_and_subnet_mask_33.json              |  3 +
 .../ipv4_and_subnet_mask_33.toml              | 18 +++++
 proxmox-installer-common/src/utils.rs         | 81 ++++++++++++++-----
 8 files changed, 143 insertions(+), 20 deletions(-)
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer/ipv4_and_subnet_31.json
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer/ipv4_and_subnet_31.toml
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_addr_is_network.json
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_addr_is_network.toml
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_mask_33.json
 create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_mask_33.toml

diff --git a/proxmox-auto-installer/tests/parse-answer.rs b/proxmox-auto-installer/tests/parse-answer.rs
index f560483..6f627a0 100644
--- a/proxmox-auto-installer/tests/parse-answer.rs
+++ b/proxmox-auto-installer/tests/parse-answer.rs
@@ -127,6 +127,7 @@ mod tests {
             fqdn_from_dhcp_no_dhcp_domain_with_default_domain,
             full_fqdn_from_dhcp_with_default_domain,
             hashed_root_password,
+            ipv4_and_subnet_31,
             minimal,
             nic_matching,
             specific_nic,
@@ -146,6 +147,8 @@ mod tests {
             duplicate_disk,
             fqdn_from_dhcp_no_default_domain,
             fqdn_hostname_only,
+            ipv4_and_subnet_addr_is_network,
+            ipv4_and_subnet_mask_33,
             lvm_maxroot_greater_than_maximum,
             lvm_swapsize_greater_than_hdsize,
             no_fqdn_from_dhcp,
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/ipv4_and_subnet_31.json b/proxmox-auto-installer/tests/resources/parse_answer/ipv4_and_subnet_31.json
new file mode 100644
index 0000000..2a475fe
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer/ipv4_and_subnet_31.json
@@ -0,0 +1,19 @@
+{
+  "autoreboot": 1,
+  "cidr": "10.10.10.10/31",
+  "country": "at",
+  "dns": "10.10.10.1",
+  "domain": "testinstall",
+  "filesys": "ext4",
+  "gateway": "10.10.10.1",
+  "hdsize": 223.57088470458984,
+  "existing_storage_auto_rename": 1,
+  "hostname": "pveauto",
+  "keymap": "de",
+  "mailto": "mail@no.invalid",
+  "mngmt_nic": "enp129s0f1np1",
+  "root_password": { "plain": "12345678" },
+  "target_hd": "/dev/sda",
+  "timezone": "Europe/Vienna",
+  "first_boot": { "enabled": 0 }
+}
diff --git a/proxmox-auto-installer/tests/resources/parse_answer/ipv4_and_subnet_31.toml b/proxmox-auto-installer/tests/resources/parse_answer/ipv4_and_subnet_31.toml
new file mode 100644
index 0000000..0ca6ca5
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer/ipv4_and_subnet_31.toml
@@ -0,0 +1,18 @@
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "pveauto.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root_password = "12345678"
+
+[network]
+source = "from-answer"
+cidr = "10.10.10.10/31"
+dns = "10.10.10.1"
+gateway = "10.10.10.1"
+filter.ID_NET_NAME = "enp129s0f1np1"
+
+[disk-setup]
+filesystem = "ext4"
+disk_list = ["sda"]
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_addr_is_network.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_addr_is_network.json
new file mode 100644
index 0000000..b28699d
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_addr_is_network.json
@@ -0,0 +1,3 @@
+{
+  "parse-error": "error parsing answer.toml: Invalid CIDR: address is a network address"
+}
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_addr_is_network.toml b/proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_addr_is_network.toml
new file mode 100644
index 0000000..7a9141b
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_addr_is_network.toml
@@ -0,0 +1,18 @@
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "pveauto.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root_password = "12345678"
+
+[network]
+source = "from-answer"
+cidr = "10.10.10.32/27"
+dns = "10.10.10.1"
+gateway = "10.10.10.1"
+filter.ID_NET_NAME = "enp129s0f1np1"
+
+[disk-setup]
+filesystem = "ext4"
+disk_list = ["sda"]
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_mask_33.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_mask_33.json
new file mode 100644
index 0000000..6b2888b
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_mask_33.json
@@ -0,0 +1,3 @@
+{
+    "parse-error": "error parsing answer.toml: Invalid CIDR: mask cannot be greater than 32"
+}
diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_mask_33.toml b/proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_mask_33.toml
new file mode 100644
index 0000000..16a560f
--- /dev/null
+++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/ipv4_and_subnet_mask_33.toml
@@ -0,0 +1,18 @@
+[global]
+keyboard = "de"
+country = "at"
+fqdn = "pveauto.testinstall"
+mailto = "mail@no.invalid"
+timezone = "Europe/Vienna"
+root_password = "12345678"
+
+[network]
+source = "from-answer"
+cidr = "10.10.10.10/33"
+dns = "10.10.10.1"
+gateway = "10.10.10.1"
+filter.ID_NET_NAME = "enp129s0f1np1"
+
+[disk-setup]
+filesystem = "ext4"
+disk_list = ["sda"]
diff --git a/proxmox-installer-common/src/utils.rs b/proxmox-installer-common/src/utils.rs
index 1fe6a74..fea98db 100644
--- a/proxmox-installer-common/src/utils.rs
+++ b/proxmox-installer-common/src/utils.rs
@@ -1,7 +1,7 @@
 use std::{
+    error::Error,
     fmt,
     net::{AddrParseError, IpAddr},
-    num::ParseIntError,
     str::FromStr,
 };
 
@@ -15,20 +15,26 @@ pub enum CidrAddressParseError {
     /// The IP address part could not be parsed.
     InvalidAddr(AddrParseError),
     /// The mask could not be parsed.
-    InvalidMask(Option<ParseIntError>),
+    InvalidMask(Box<dyn Error>),
+    /// The IP address is a network address.
+    IsNetworkAddr,
+    /// The IP address is a broadcast address.
+    IsBroadcastAddr,
 }
 
 impl fmt::Display for CidrAddressParseError {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let msg = match &self {
+        write!(f, "Invalid CIDR: ")?;
+
+        match self {
             CidrAddressParseError::NoDelimiter => {
-                String::from("No delimiter for separating address and mask was found")
+                write!(f, "no delimiter for separating address and mask was found")
             }
-            CidrAddressParseError::InvalidAddr(addr_parse_error) => format!("{addr_parse_error}"),
-            CidrAddressParseError::InvalidMask(parse_int_error) => format!("{:?}", parse_int_error),
-        };
-
-        write!(f, "Invalid CIDR: {msg}")
+            CidrAddressParseError::InvalidAddr(err) => write!(f, "{err}"),
+            CidrAddressParseError::InvalidMask(err) => write!(f, "{err}"),
+            CidrAddressParseError::IsNetworkAddr => write!(f, "address is a network address"),
+            CidrAddressParseError::IsBroadcastAddr => write!(f, "address is a broadcast address"),
+        }
     }
 }
 
@@ -61,11 +67,10 @@ impl CidrAddress {
     pub fn new<T: Into<IpAddr>>(addr: T, mask: usize) -> Result<Self, CidrAddressParseError> {
         let addr = addr.into();
 
-        if mask > mask_limit(&addr) {
-            Err(CidrAddressParseError::InvalidMask(None))
-        } else {
-            Ok(Self { addr, mask })
-        }
+        check_mask_limit(&addr, mask)?;
+        check_addr_valid_in_subnet(&addr, mask)?;
+
+        Ok(Self { addr, mask })
     }
 
     /// Returns only the IP address part of the address.
@@ -101,13 +106,12 @@ impl FromStr for CidrAddress {
 
         let mask = mask
             .parse()
-            .map_err(|err| CidrAddressParseError::InvalidMask(Some(err)))?;
+            .map_err(|err| CidrAddressParseError::InvalidMask(Box::new(err)))?;
 
-        if mask > mask_limit(&addr) {
-            Err(CidrAddressParseError::InvalidMask(None))
-        } else {
-            Ok(Self { addr, mask })
-        }
+        check_mask_limit(&addr, mask)?;
+        check_addr_valid_in_subnet(&addr, mask)?;
+
+        Ok(Self { addr, mask })
     }
 }
 
@@ -133,6 +137,43 @@ fn mask_limit(addr: &IpAddr) -> usize {
     if addr.is_ipv4() { 32 } else { 128 }
 }
 
+fn check_mask_limit(addr: &IpAddr, mask: usize) -> Result<(), CidrAddressParseError> {
+    return if mask > mask_limit(&addr) {
+        Err(CidrAddressParseError::InvalidMask(
+            "mask cannot be greater than 32".into(),
+        ))
+    } else {
+        Ok(())
+    };
+}
+
+fn check_addr_valid_in_subnet(addr: &IpAddr, mask: usize) -> Result<(), CidrAddressParseError> {
+    match &addr {
+        IpAddr::V4(addr_v4) => {
+            if mask >= 31 {
+                return Ok(());
+            }
+
+            let num_host_bits = 32 - mask;
+            let host_part_mask = (1u32 << num_host_bits) - 1;
+
+            let ip_bits = u32::from_be_bytes(addr_v4.octets());
+
+            let network_addr = ip_bits & !host_part_mask;
+            let broadcast_addr = network_addr | host_part_mask;
+
+            if ip_bits == network_addr {
+                Err(CidrAddressParseError::IsNetworkAddr)
+            } else if ip_bits == broadcast_addr {
+                Err(CidrAddressParseError::IsBroadcastAddr)
+            } else {
+                Ok(())
+            }
+        }
+        IpAddr::V6(_) => Ok(()),
+    }
+}
+
 /// Possible errors that might occur when parsing FQDNs.
 #[derive(Debug, Eq, PartialEq)]
 pub enum FqdnParseError {
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [pve-devel] [RFC PATCH pve-installer v2 6/6] common: add checks for valid subnet mask and IPv4 address within subnet
  2025-04-29 14:09 ` [pve-devel] [RFC PATCH pve-installer v2 6/6] common: add checks for valid subnet mask and IPv4 address within subnet Michael Köppl
@ 2025-05-06  9:21   ` Christoph Heiss
  2025-05-09  8:51     ` Michael Köppl
  0 siblings, 1 reply; 11+ messages in thread
From: Christoph Heiss @ 2025-05-06  9:21 UTC (permalink / raw)
  To: Michael Köppl; +Cc: Proxmox VE development discussion

Apart from the one inline comment, looks good to me.

IMO it's not too restrictive, since IPv4 network/broadcast addresses
shouldn't really be used (and probably introduce subtle breakages)
outside of /31 prefixes anyway.

On Tue Apr 29, 2025 at 4:09 PM CEST, Michael Köppl wrote:
> Add checks for valid subnet mask (greater than /0 and at most /32 for
> IPv4). In addition, check if the address entered by the user is valid
> within the given subnet, i.e. not a network address or broadcast
> address. /31 is considered an exception in accordance with RFC3021 [0],
> considering any of the 2 available addresses to be valid host addresses.
>
> [0] https://datatracker.ietf.org/doc/html/rfc3021
>
> Signed-off-by: Michael Köppl <m.koeppl@proxmox.com>
> ---
> [..]
> diff --git a/proxmox-installer-common/src/utils.rs b/proxmox-installer-common/src/utils.rs
> index 1fe6a74..fea98db 100644
> --- a/proxmox-installer-common/src/utils.rs
> +++ b/proxmox-installer-common/src/utils.rs
> [..]
> @@ -133,6 +137,43 @@ fn mask_limit(addr: &IpAddr) -> usize {
>      if addr.is_ipv4() { 32 } else { 128 }
>  }
>
> +fn check_mask_limit(addr: &IpAddr, mask: usize) -> Result<(), CidrAddressParseError> {
> +    return if mask > mask_limit(&addr) {
> +        Err(CidrAddressParseError::InvalidMask(
> +            "mask cannot be greater than 32".into(),

s/32/mask_limit(&addr)/g

> +        ))
> +    } else {
> +        Ok(())
> +    };
> +}


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [pve-devel] [PATCH pve-installer v2 3/6] close #5887: add sanity check for LVM swapsize and maxroot
  2025-04-29 14:09 ` [pve-devel] [PATCH pve-installer v2 3/6] close #5887: add sanity check for LVM swapsize and maxroot Michael Köppl
@ 2025-05-06 11:48   ` Christoph Heiss
  2025-05-09 11:07     ` Michael Köppl
  0 siblings, 1 reply; 11+ messages in thread
From: Christoph Heiss @ 2025-05-06 11:48 UTC (permalink / raw)
  To: Michael Köppl; +Cc: Proxmox VE development discussion

After testing this change and thinking about the maxroot change again,
$hdsize / 4 doesn't really make sense. E.g. for an (unrealistically
small, but still) disk of 8 GiB; if its unset, pve-root will be ~6.5 GiB
in size, with the limit of 2 GiB, the installation fails due to
ENOSPACE.

The default calculations try really hard to make installations possible
even on small disks, in Proxmox/Install.pm:create_lvm_volumes()

So I'm not sure if we really should restrict it that much, or rather
relax it in the documentation.

On Tue Apr 29, 2025 at 4:09 PM CEST, Michael Köppl wrote:
> Check that the configured swapsize is not greater than hdsize / 8 as
> stated in the admin guide [0]. Additionally check that maxroot is at
> most hdsize / 4. Define the behavior for the auto-installer as well as
> the TUI and GUI installers.
>
> [0] https://pve.proxmox.com/pve-docs/pve-admin-guide.html#advanced_lvm_options
>
> Signed-off-by: Michael Köppl <m.koeppl@proxmox.com>
> ---
> Note regarding the change around set_hdsize in proxinstall: Before this
> change, the hdsize was only set if the user manually changed it. If the
> user did not change it, any checks against hdsize would check against
> undefined.
>
>  Proxmox/Install.pm                            | 16 ++++++
>  proxinstall                                   |  5 +-
>  proxmox-auto-installer/src/utils.rs           | 16 ++++++
>  proxmox-auto-installer/tests/parse-answer.rs  |  4 +-
>  .../lvm_maxroot_greater_than_maximum.json     |  3 ++
>  .../lvm_maxroot_greater_than_maximum.toml     | 16 ++++++
>  .../lvm_swapsize_greater_than_hdsize.json     |  3 ++
>  .../lvm_swapsize_greater_than_hdsize.toml     | 16 ++++++
>  proxmox-installer-common/src/disk_checks.rs   | 52 ++++++++++++++++++-
>  proxmox-tui-installer/src/views/bootdisk.rs   |  6 ++-
>  10 files changed, 133 insertions(+), 4 deletions(-)
>  create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_maxroot_greater_than_maximum.json
>  create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_maxroot_greater_than_maximum.toml
>  create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_swapsize_greater_than_hdsize.json
>  create mode 100644 proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_swapsize_greater_than_hdsize.toml
>
> diff --git a/Proxmox/Install.pm b/Proxmox/Install.pm
> index f673604..7c55d1f 100644
> --- a/Proxmox/Install.pm
> +++ b/Proxmox/Install.pm
> @@ -592,6 +592,22 @@ sub compute_swapsize {
>      return $swapsize_kb;
>  }
>
> +sub swapsize_check {
> +    my ($hdsize) = @_;
> +    my $swapsize = Proxmox::Install::Config::get_swapsize();
> +    my $threshold = $hdsize / 8;
> +    die "swap size ${swapsize} GiB cannot be greater than ${threshold} GiB (hard disk size / 8)\n"
> +	if $swapsize > $threshold;
> +}
> +
> +sub maxroot_size_check {
> +    my ($hdsize) = @_;
> +    my $maxroot = Proxmox::Install::Config::get_maxroot();
> +    my $threshold = $hdsize / 4;
> +    die "maximum root volume size ${maxroot} GiB cannot be greater than ${threshold} GiB (hard disk size / 4)\n"
> +	if $maxroot > $threshold;
> +}
> +
>  my sub chroot_chown {
>      my ($root, $path, %param) = @_;
>
> diff --git a/proxinstall b/proxinstall
> index bc9ade6..e9ff6e8 100755
> --- a/proxinstall
> +++ b/proxinstall
> @@ -1406,7 +1406,7 @@ sub create_hdoption_view {
>
>      my $tmp;
>
> -    if (($tmp = &$get_float($spinbutton_hdsize)) && ($tmp != $hdsize)) {
> +    if (defined($tmp = &$get_float($spinbutton_hdsize))) {
>  	Proxmox::Install::Config::set_hdsize($tmp);
>      } else {
>  	Proxmox::Install::Config::set_hdsize(undef);
> @@ -1521,9 +1521,12 @@ sub create_hdsel_view {
>  	    $target_hds = [ map { $_->[1] } @$devlist ];
>  	} else {
>  	    my $target_hd = Proxmox::Install::Config::get_target_hd();
> +	    my $hdsize = Proxmox::Install::Config::get_hdsize();
>  	    eval {
>  		my $target_block_size = Proxmox::Sys::Block::logical_blocksize($target_hd);
>  		Proxmox::Install::legacy_bios_4k_check($target_block_size);
> +		Proxmox::Install::swapsize_check($hdsize);
> +		Proxmox::Install::maxroot_size_check($hdsize);
>  	    };
>  	    if (my $err = $@) {
>  		Proxmox::UI::message("Warning: $err\n");
> diff --git a/proxmox-auto-installer/src/utils.rs b/proxmox-auto-installer/src/utils.rs
> index d6bc6e3..75696bd 100644
> --- a/proxmox-auto-installer/src/utils.rs
> +++ b/proxmox-auto-installer/src/utils.rs
> @@ -13,6 +13,7 @@ use crate::{
>  };
>  use proxmox_installer_common::{
>      ROOT_PASSWORD_MIN_LENGTH,
> +    disk_checks::{check_maxroot, check_swapsize},
>      options::{FsType, NetworkOptions, ZfsChecksumOption, ZfsCompressOption, email_validate},
>      setup::{
>          InstallBtrfsOption, InstallConfig, InstallFirstBootSetup, InstallRootPassword,
> @@ -396,6 +397,21 @@ pub fn verify_disks_settings(answer: &Answer) -> Result<()> {
>              );
>          }
>      }
> +
> +    if let answer::FsOptions::LVM(lvm) = &answer.disks.fs_options {
> +        if let Some((swapsize, hdsize)) = lvm.swapsize.zip(lvm.hdsize) {
> +            if let Err(err) = check_swapsize(swapsize, hdsize) {
> +                bail!(err);
> +            }
> +        }
> +
> +        if let Some((maxroot, hdsize)) = lvm.maxroot.zip(lvm.hdsize) {
> +            if let Err(err) = check_maxroot(maxroot, hdsize) {
> +                bail!(err);
> +            }
> +        }
> +    }
> +
>      Ok(())
>  }
>
> diff --git a/proxmox-auto-installer/tests/parse-answer.rs b/proxmox-auto-installer/tests/parse-answer.rs
> index 92dba63..e615672 100644
> --- a/proxmox-auto-installer/tests/parse-answer.rs
> +++ b/proxmox-auto-installer/tests/parse-answer.rs
> @@ -7,7 +7,7 @@ use proxmox_auto_installer::udevinfo::UdevInfo;
>  use proxmox_auto_installer::utils::parse_answer;
>
>  use proxmox_installer_common::setup::{
> -    LocaleInfo, RuntimeInfo, SetupInfo, load_installer_setup_files, read_json,
> +    load_installer_setup_files, read_json, LocaleInfo, RuntimeInfo, SetupInfo,
>  };
>
>  fn get_test_resource_path() -> Result<PathBuf, String> {
> @@ -145,6 +145,8 @@ mod tests {
>              btrfs_raid_single_disk,
>              fqdn_from_dhcp_no_default_domain,
>              fqdn_hostname_only,
> +            lvm_maxroot_greater_than_maximum,
> +            lvm_swapsize_greater_than_hdsize,
>              no_fqdn_from_dhcp,
>              no_root_password_set,
>              short_password,
> diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_maxroot_greater_than_maximum.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_maxroot_greater_than_maximum.json
> new file mode 100644
> index 0000000..bab12e6
> --- /dev/null
> +++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_maxroot_greater_than_maximum.json
> @@ -0,0 +1,3 @@
> +{
> +  "error": "Maximum root volume size 8.01 GiB cannot be greater than 8 GiB (hard disk size / 4)"
> +}
> diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_maxroot_greater_than_maximum.toml b/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_maxroot_greater_than_maximum.toml
> new file mode 100644
> index 0000000..e934d29
> --- /dev/null
> +++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_maxroot_greater_than_maximum.toml
> @@ -0,0 +1,16 @@
> +[global]
> +keyboard = "de"
> +country = "at"
> +fqdn = "btrfs-raid-single-disk.fail.testinstall"
> +mailto = "mail@no.invalid"
> +timezone = "Europe/Vienna"
> +root-password = "12345678"
> +
> +[network]
> +source = "from-dhcp"
> +
> +[disk-setup]
> +filesystem = "ext4"
> +lvm.maxroot = 8.01
> +lvm.hdsize = 32
> +disk-list = ["sda"]
> diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_swapsize_greater_than_hdsize.json b/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_swapsize_greater_than_hdsize.json
> new file mode 100644
> index 0000000..aa4f7fe
> --- /dev/null
> +++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_swapsize_greater_than_hdsize.json
> @@ -0,0 +1,3 @@
> +{
> +  "error": "Swap size 4.01 GiB cannot be greater than 4 GiB (hard disk size / 8)"
> +}
> diff --git a/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_swapsize_greater_than_hdsize.toml b/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_swapsize_greater_than_hdsize.toml
> new file mode 100644
> index 0000000..ffe16dc
> --- /dev/null
> +++ b/proxmox-auto-installer/tests/resources/parse_answer_fail/lvm_swapsize_greater_than_hdsize.toml
> @@ -0,0 +1,16 @@
> +[global]
> +keyboard = "de"
> +country = "at"
> +fqdn = "btrfs-raid-single-disk.fail.testinstall"
> +mailto = "mail@no.invalid"
> +timezone = "Europe/Vienna"
> +root-password = "12345678"
> +
> +[network]
> +source = "from-dhcp"
> +
> +[disk-setup]
> +filesystem = "ext4"
> +lvm.swapsize = 4.01
> +lvm.hdsize = 32
> +disk-list = ["sda"]
> diff --git a/proxmox-installer-common/src/disk_checks.rs b/proxmox-installer-common/src/disk_checks.rs
> index d535837..77104ae 100644
> --- a/proxmox-installer-common/src/disk_checks.rs
> +++ b/proxmox-installer-common/src/disk_checks.rs
> @@ -1,6 +1,6 @@
>  use std::collections::HashSet;
>
> -use crate::options::Disk;
> +use crate::options::{Disk, LvmBootdiskOptions};
>  use crate::setup::BootType;
>
>  /// Checks a list of disks for duplicate entries, using their index as key.
> @@ -49,6 +49,56 @@ pub fn check_disks_4kn_legacy_boot(boot_type: BootType, disks: &[Disk]) -> Resul
>      Ok(())
>  }
>
> +/// Checks whether the configured swap size exceeds the allowed threshold.
> +///
> +/// # Arguments
> +///
> +/// * `swapsize` - The size of the swap in GiB
> +/// * `hdsize` - The total size of the hard disk in GiB
> +pub fn check_swapsize(swapsize: f64, hdsize: f64) -> Result<(), String> {
> +    let threshold = hdsize / 8.0;
> +    if swapsize > threshold {
> +        return Err(format!(
> +            "Swap size {swapsize} GiB cannot be greater than {threshold} GiB (hard disk size / 8)",
> +        ));
> +    }
> +    Ok(())
> +}
> +
> +/// Checks whether the configured root volume size exceeds the allowed threshold.
> +///
> +/// # Arguments
> +///
> +/// * `maxroot` - The size of the root volume in GiB
> +/// * `hdsize` - The total size of the hard disk in GiB
> +pub fn check_maxroot(maxroot: f64, hdsize: f64) -> Result<(), String> {
> +    let threshold = hdsize / 4.0;
> +    if maxroot > threshold {
> +        return Err(format!(
> +            "Maximum root volume size {maxroot} GiB cannot be greater than {threshold} GiB (hard disk size / 4)",
> +        ));
> +    }
> +    Ok(())
> +}
> +
> +/// Checks whether a user-supplied LVM setup is valid or not, such as the swapsize or maxroot not
> +/// exceeding certain thresholds.
> +///
> +/// # Arguments
> +///
> +/// * `bootdisk_opts` - The LVM options set by the user.
> +pub fn check_lvm_bootdisk_opts(bootdisk_opts: &LvmBootdiskOptions) -> Result<(), String> {
> +    if let Some(swap_size) = bootdisk_opts.swap_size {
> +        check_swapsize(swap_size, bootdisk_opts.total_size)?;
> +    }
> +
> +    if let Some(max_root_size) = bootdisk_opts.max_root_size {
> +        check_maxroot(max_root_size, bootdisk_opts.total_size)?;
> +    }
> +
> +    Ok(())
> +}
> +
>  #[cfg(test)]
>  mod tests {
>      use crate::options::{BtrfsRaidLevel, ZfsRaidLevel};
> diff --git a/proxmox-tui-installer/src/views/bootdisk.rs b/proxmox-tui-installer/src/views/bootdisk.rs
> index e87b040..b94cf38 100644
> --- a/proxmox-tui-installer/src/views/bootdisk.rs
> +++ b/proxmox-tui-installer/src/views/bootdisk.rs
> @@ -17,7 +17,9 @@ use crate::InstallerState;
>  use crate::options::FS_TYPES;
>
>  use proxmox_installer_common::{
> -    disk_checks::{check_disks_4kn_legacy_boot, check_for_duplicate_disks},
> +    disk_checks::{
> +        check_disks_4kn_legacy_boot, check_for_duplicate_disks, check_lvm_bootdisk_opts,
> +    },
>      options::{
>          AdvancedBootdiskOptions, BTRFS_COMPRESS_OPTIONS, BootdiskOptions, BtrfsBootdiskOptions,
>          Disk, FsType, LvmBootdiskOptions, ZFS_CHECKSUM_OPTIONS, ZFS_COMPRESS_OPTIONS,
> @@ -261,6 +263,8 @@ impl AdvancedBootdiskOptionsView {
>                  .get_values()
>                  .ok_or("Failed to retrieve advanced bootdisk options")?;
>
> +            check_lvm_bootdisk_opts(&advanced).map_err(|err| format!("{fstype}: {err}"))?;
> +
>              Ok(BootdiskOptions {
>                  disks: vec![disk],
>                  fstype,



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [pve-devel] [RFC PATCH pve-installer v2 6/6] common: add checks for valid subnet mask and IPv4 address within subnet
  2025-05-06  9:21   ` Christoph Heiss
@ 2025-05-09  8:51     ` Michael Köppl
  0 siblings, 0 replies; 11+ messages in thread
From: Michael Köppl @ 2025-05-09  8:51 UTC (permalink / raw)
  To: Christoph Heiss; +Cc: Proxmox VE development discussion

On 5/6/25 11:21, Christoph Heiss wrote:
> Apart from the one inline comment, looks good to me.
> 
> IMO it's not too restrictive, since IPv4 network/broadcast addresses
> shouldn't really be used (and probably introduce subtle breakages)
> outside of /31 prefixes anyway.
> 
> On Tue Apr 29, 2025 at 4:09 PM CEST, Michael Köppl wrote:
>> Add checks for valid subnet mask (greater than /0 and at most /32 for
>> IPv4). In addition, check if the address entered by the user is valid
>> within the given subnet, i.e. not a network address or broadcast
>> address. /31 is considered an exception in accordance with RFC3021 [0],
>> considering any of the 2 available addresses to be valid host addresses.
>>
>> [0] https://datatracker.ietf.org/doc/html/rfc3021
>>
>> Signed-off-by: Michael Köppl <m.koeppl@proxmox.com>
>> ---
>> [..]
>> diff --git a/proxmox-installer-common/src/utils.rs b/proxmox-installer-common/src/utils.rs
>> index 1fe6a74..fea98db 100644
>> --- a/proxmox-installer-common/src/utils.rs
>> +++ b/proxmox-installer-common/src/utils.rs
>> [..]
>> @@ -133,6 +137,43 @@ fn mask_limit(addr: &IpAddr) -> usize {
>>      if addr.is_ipv4() { 32 } else { 128 }
>>  }
>>
>> +fn check_mask_limit(addr: &IpAddr, mask: usize) -> Result<(), CidrAddressParseError> {
>> +    return if mask > mask_limit(&addr) {
>> +        Err(CidrAddressParseError::InvalidMask(
>> +            "mask cannot be greater than 32".into(),
> 
> s/32/mask_limit(&addr)/g

Will update this bit when I send a v3 including a better solution for
the maxroot check. Thanks!

> 
>> +        ))
>> +    } else {
>> +        Ok(())
>> +    };
>> +}



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [pve-devel] [PATCH pve-installer v2 3/6] close #5887: add sanity check for LVM swapsize and maxroot
  2025-05-06 11:48   ` Christoph Heiss
@ 2025-05-09 11:07     ` Michael Köppl
  0 siblings, 0 replies; 11+ messages in thread
From: Michael Köppl @ 2025-05-09 11:07 UTC (permalink / raw)
  To: Christoph Heiss; +Cc: Proxmox VE development discussion

On 5/6/25 13:48, Christoph Heiss wrote:
> After testing this change and thinking about the maxroot change again,
> $hdsize / 4 doesn't really make sense. E.g. for an (unrealistically
> small, but still) disk of 8 GiB; if its unset, pve-root will be ~6.5 GiB
> in size, with the limit of 2 GiB, the installation fails due to
> ENOSPACE.
> 
> The default calculations try really hard to make installations possible
> even on small disks, in Proxmox/Install.pm:create_lvm_volumes()
> 
> So I'm not sure if we really should restrict it that much, or rather
> relax it in the documentation.

Thought a bit about this. I agree that the sanity check should not
entirely stop users from creating setups that would work. An alternative
approach might be not to enforce the maxroot limit of $hdsize / 4 for
smaller disks. create_lvm_volumes() considers 12 GiB to be small, if I'm
not mistaken, and basically does a best-effort install. I'm not sure if
I like setting such an arbitrary limit for a sanity check, though. It
makes the sanity check intransparent if users cannot configure a root
volume size greater than 4 GiB if their hdsize is 16 GiB, but at the
same time are free to set it to 6 GiB if the hdsize is 8 GiB. What do
you think?

Another alternative might be to simply display a warning that doesn't
stop users from installing, i.e. "Recommended maximum root volume size
is hdsize / 4".

In both cases the documentation should be updated to reflect that.


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 11+ messages in thread

end of thread, other threads:[~2025-05-09 11:07 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-04-29 14:09 [pve-devel] [PATCH installer v2 0/6] add early disk and network sanity checks Michael Köppl
2025-04-29 14:09 ` [pve-devel] [PATCH pve-installer v2 1/6] auto: add early answer file sanity check for RAID configurations Michael Köppl
2025-04-29 14:09 ` [pve-devel] [PATCH pve-installer v2 2/6] move RAID setup checks to RAID level enum implementations Michael Köppl
2025-04-29 14:09 ` [pve-devel] [PATCH pve-installer v2 3/6] close #5887: add sanity check for LVM swapsize and maxroot Michael Köppl
2025-05-06 11:48   ` Christoph Heiss
2025-05-09 11:07     ` Michael Köppl
2025-04-29 14:09 ` [pve-devel] [PATCH pve-installer v2 4/6] auto: add check for duplicate disks in answer file Michael Köppl
2025-04-29 14:09 ` [pve-devel] [PATCH pve-installer v2 5/6] common: add more descriptive errors for invalid network configs Michael Köppl
2025-04-29 14:09 ` [pve-devel] [RFC PATCH pve-installer v2 6/6] common: add checks for valid subnet mask and IPv4 address within subnet Michael Köppl
2025-05-06  9:21   ` Christoph Heiss
2025-05-09  8:51     ` Michael Köppl

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal