[pve-devel] [PATCH 07/12] tui: switch to common crate

Aaron Lauterer a.lauterer at proxmox.com
Wed Oct 25 18:00:06 CEST 2023


by switching dependencies and deleting doubled code to avoid ambiguities
within the same scope.

Signed-off-by: Aaron Lauterer <a.lauterer at proxmox.com>
---
 proxmox-tui-installer/src/main.rs           |  13 +-
 proxmox-tui-installer/src/options.rs        | 403 +-------------------
 proxmox-tui-installer/src/setup.rs          | 316 +--------------
 proxmox-tui-installer/src/system.rs         |   2 +-
 proxmox-tui-installer/src/views/bootdisk.rs | 248 +-----------
 proxmox-tui-installer/src/views/mod.rs      |   2 +-
 proxmox-tui-installer/src/views/timezone.rs |   4 +-
 7 files changed, 44 insertions(+), 944 deletions(-)

diff --git a/proxmox-tui-installer/src/main.rs b/proxmox-tui-installer/src/main.rs
index 81fe3ca..875a33a 100644
--- a/proxmox-tui-installer/src/main.rs
+++ b/proxmox-tui-installer/src/main.rs
@@ -28,16 +28,19 @@ use cursive::{
 use regex::Regex;
 
 mod options;
-use options::*;
+use options::InstallerOptions;
+
+use proxmox_installer_common::{
+    options::{BootdiskOptions, NetworkOptions, PasswordOptions, TimezoneOptions},
+    setup::{LocaleInfo, ProxmoxProduct, RuntimeInfo, SetupInfo},
+    utils::Fqdn,
+};
 
 mod setup;
-use setup::{InstallConfig, LocaleInfo, ProxmoxProduct, RuntimeInfo, SetupInfo};
+use setup::InstallConfig;
 
 mod system;
 
-mod utils;
-use utils::Fqdn;
-
 mod views;
 use views::{
     BootdiskOptionsView, CidrAddressEditView, FormView, TableView, TableViewItem,
diff --git a/proxmox-tui-installer/src/options.rs b/proxmox-tui-installer/src/options.rs
index 85b39b8..221ad01 100644
--- a/proxmox-tui-installer/src/options.rs
+++ b/proxmox-tui-installer/src/options.rs
@@ -1,77 +1,12 @@
-use std::net::{IpAddr, Ipv4Addr};
-use std::{cmp, fmt};
-
-use crate::setup::{LocaleInfo, NetworkInfo, RuntimeInfo, SetupInfo};
-use crate::utils::{CidrAddress, Fqdn};
 use crate::SummaryOption;
 
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
-pub enum BtrfsRaidLevel {
-    Raid0,
-    Raid1,
-    Raid10,
-}
-
-impl fmt::Display for BtrfsRaidLevel {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        use BtrfsRaidLevel::*;
-        match self {
-            Raid0 => write!(f, "RAID0"),
-            Raid1 => write!(f, "RAID1"),
-            Raid10 => write!(f, "RAID10"),
-        }
-    }
-}
-
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
-pub enum ZfsRaidLevel {
-    Raid0,
-    Raid1,
-    Raid10,
-    RaidZ,
-    RaidZ2,
-    RaidZ3,
-}
-
-impl fmt::Display for ZfsRaidLevel {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        use ZfsRaidLevel::*;
-        match self {
-            Raid0 => write!(f, "RAID0"),
-            Raid1 => write!(f, "RAID1"),
-            Raid10 => write!(f, "RAID10"),
-            RaidZ => write!(f, "RAIDZ-1"),
-            RaidZ2 => write!(f, "RAIDZ-2"),
-            RaidZ3 => write!(f, "RAIDZ-3"),
-        }
-    }
-}
-
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
-pub enum FsType {
-    Ext4,
-    Xfs,
-    Zfs(ZfsRaidLevel),
-    Btrfs(BtrfsRaidLevel),
-}
-
-impl FsType {
-    pub fn is_btrfs(&self) -> bool {
-        matches!(self, FsType::Btrfs(_))
-    }
-}
-
-impl fmt::Display for FsType {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        use FsType::*;
-        match self {
-            Ext4 => write!(f, "ext4"),
-            Xfs => write!(f, "XFS"),
-            Zfs(level) => write!(f, "ZFS ({level})"),
-            Btrfs(level) => write!(f, "Btrfs ({level})"),
-        }
-    }
-}
+use proxmox_installer_common::{
+    setup::LocaleInfo,
+    options::{
+        BootdiskOptions, BtrfsRaidLevel, FsType, NetworkOptions, TimezoneOptions,
+        PasswordOptions, ZfsRaidLevel,
+    },
+};
 
 pub const FS_TYPES: &[FsType] = {
     use FsType::*;
@@ -90,320 +25,6 @@ pub const FS_TYPES: &[FsType] = {
     ]
 };
 
-#[derive(Clone, Debug)]
-pub struct LvmBootdiskOptions {
-    pub total_size: f64,
-    pub swap_size: Option<f64>,
-    pub max_root_size: Option<f64>,
-    pub max_data_size: Option<f64>,
-    pub min_lvm_free: Option<f64>,
-}
-
-impl LvmBootdiskOptions {
-    pub fn defaults_from(disk: &Disk) -> Self {
-        Self {
-            total_size: disk.size,
-            swap_size: None,
-            max_root_size: None,
-            max_data_size: None,
-            min_lvm_free: None,
-        }
-    }
-}
-
-#[derive(Clone, Debug)]
-pub struct BtrfsBootdiskOptions {
-    pub disk_size: f64,
-    pub selected_disks: Vec<usize>,
-}
-
-impl BtrfsBootdiskOptions {
-    /// This panics if the provided slice is empty.
-    pub fn defaults_from(disks: &[Disk]) -> Self {
-        let disk = &disks[0];
-        Self {
-            disk_size: disk.size,
-            selected_disks: (0..disks.len()).collect(),
-        }
-    }
-}
-
-#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
-pub enum ZfsCompressOption {
-    #[default]
-    On,
-    Off,
-    Lzjb,
-    Lz4,
-    Zle,
-    Gzip,
-    Zstd,
-}
-
-impl fmt::Display for ZfsCompressOption {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "{}", format!("{self:?}").to_lowercase())
-    }
-}
-
-impl From<&ZfsCompressOption> for String {
-    fn from(value: &ZfsCompressOption) -> Self {
-        value.to_string()
-    }
-}
-
-pub const ZFS_COMPRESS_OPTIONS: &[ZfsCompressOption] = {
-    use ZfsCompressOption::*;
-    &[On, Off, Lzjb, Lz4, Zle, Gzip, Zstd]
-};
-
-#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
-pub enum ZfsChecksumOption {
-    #[default]
-    On,
-    Off,
-    Fletcher2,
-    Fletcher4,
-    Sha256,
-}
-
-impl fmt::Display for ZfsChecksumOption {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "{}", format!("{self:?}").to_lowercase())
-    }
-}
-
-impl From<&ZfsChecksumOption> for String {
-    fn from(value: &ZfsChecksumOption) -> Self {
-        value.to_string()
-    }
-}
-
-pub const ZFS_CHECKSUM_OPTIONS: &[ZfsChecksumOption] = {
-    use ZfsChecksumOption::*;
-    &[On, Off, Fletcher2, Fletcher4, Sha256]
-};
-
-#[derive(Clone, Debug)]
-pub struct ZfsBootdiskOptions {
-    pub ashift: usize,
-    pub compress: ZfsCompressOption,
-    pub checksum: ZfsChecksumOption,
-    pub copies: usize,
-    pub disk_size: f64,
-    pub selected_disks: Vec<usize>,
-}
-
-impl ZfsBootdiskOptions {
-    /// This panics if the provided slice is empty.
-    pub fn defaults_from(disks: &[Disk]) -> Self {
-        let disk = &disks[0];
-        Self {
-            ashift: 12,
-            compress: ZfsCompressOption::default(),
-            checksum: ZfsChecksumOption::default(),
-            copies: 1,
-            disk_size: disk.size,
-            selected_disks: (0..disks.len()).collect(),
-        }
-    }
-}
-
-#[derive(Clone, Debug)]
-pub enum AdvancedBootdiskOptions {
-    Lvm(LvmBootdiskOptions),
-    Zfs(ZfsBootdiskOptions),
-    Btrfs(BtrfsBootdiskOptions),
-}
-
-#[derive(Clone, Debug, PartialEq)]
-pub struct Disk {
-    pub index: String,
-    pub path: String,
-    pub model: Option<String>,
-    pub size: f64,
-    pub block_size: Option<usize>,
-}
-
-impl fmt::Display for Disk {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        // TODO: Format sizes properly with `proxmox-human-byte` once merged
-        // https://lists.proxmox.com/pipermail/pbs-devel/2023-May/006125.html
-        f.write_str(&self.path)?;
-        if let Some(model) = &self.model {
-            // FIXME: ellipsize too-long names?
-            write!(f, " ({model})")?;
-        }
-        write!(f, " ({:.2} GiB)", self.size)
-    }
-}
-
-impl From<&Disk> for String {
-    fn from(value: &Disk) -> Self {
-        value.to_string()
-    }
-}
-
-impl cmp::Eq for Disk {}
-
-impl cmp::PartialOrd for Disk {
-    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
-        self.index.partial_cmp(&other.index)
-    }
-}
-
-impl cmp::Ord for Disk {
-    fn cmp(&self, other: &Self) -> cmp::Ordering {
-        self.index.cmp(&other.index)
-    }
-}
-
-#[derive(Clone, Debug)]
-pub struct BootdiskOptions {
-    pub disks: Vec<Disk>,
-    pub fstype: FsType,
-    pub advanced: AdvancedBootdiskOptions,
-}
-
-impl BootdiskOptions {
-    pub fn defaults_from(disk: &Disk) -> Self {
-        Self {
-            disks: vec![disk.clone()],
-            fstype: FsType::Ext4,
-            advanced: AdvancedBootdiskOptions::Lvm(LvmBootdiskOptions::defaults_from(disk)),
-        }
-    }
-}
-
-#[derive(Clone, Debug)]
-pub struct TimezoneOptions {
-    pub country: String,
-    pub timezone: String,
-    pub kb_layout: String,
-}
-
-impl TimezoneOptions {
-    pub fn defaults_from(runtime: &RuntimeInfo, locales: &LocaleInfo) -> Self {
-        let country = runtime.country.clone().unwrap_or_else(|| "at".to_owned());
-
-        let timezone = locales
-            .cczones
-            .get(&country)
-            .and_then(|zones| zones.get(0))
-            .cloned()
-            .unwrap_or_else(|| "UTC".to_owned());
-
-        let kb_layout = locales
-            .countries
-            .get(&country)
-            .and_then(|c| {
-                if c.kmap.is_empty() {
-                    None
-                } else {
-                    Some(c.kmap.clone())
-                }
-            })
-            .unwrap_or_else(|| "en-us".to_owned());
-
-        Self {
-            country,
-            timezone,
-            kb_layout,
-        }
-    }
-}
-
-#[derive(Clone, Debug)]
-pub struct PasswordOptions {
-    pub email: String,
-    pub root_password: String,
-}
-
-impl Default for PasswordOptions {
-    fn default() -> Self {
-        Self {
-            email: "mail at example.invalid".to_string(),
-            root_password: String::new(),
-        }
-    }
-}
-
-#[derive(Clone, Debug, PartialEq)]
-pub struct NetworkOptions {
-    pub ifname: String,
-    pub fqdn: Fqdn,
-    pub address: CidrAddress,
-    pub gateway: IpAddr,
-    pub dns_server: IpAddr,
-}
-
-impl NetworkOptions {
-    const DEFAULT_DOMAIN: &str = "example.invalid";
-
-    pub fn defaults_from(setup: &SetupInfo, network: &NetworkInfo) -> Self {
-        let mut this = Self {
-            ifname: String::new(),
-            fqdn: Self::construct_fqdn(network, setup.config.product.default_hostname()),
-            // Safety: The provided mask will always be valid.
-            address: CidrAddress::new(Ipv4Addr::UNSPECIFIED, 0).unwrap(),
-            gateway: Ipv4Addr::UNSPECIFIED.into(),
-            dns_server: Ipv4Addr::UNSPECIFIED.into(),
-        };
-
-        if let Some(ip) = network.dns.dns.first() {
-            this.dns_server = *ip;
-        }
-
-        if let Some(routes) = &network.routes {
-            let mut filled = false;
-            if let Some(gw) = &routes.gateway4 {
-                if let Some(iface) = network.interfaces.get(&gw.dev) {
-                    this.ifname = iface.name.clone();
-                    if let Some(addresses) = &iface.addresses {
-                        if let Some(addr) = addresses.iter().find(|addr| addr.is_ipv4()) {
-                            this.gateway = gw.gateway;
-                            this.address = addr.clone();
-                            filled = true;
-                        }
-                    }
-                }
-            }
-            if !filled {
-                if let Some(gw) = &routes.gateway6 {
-                    if let Some(iface) = network.interfaces.get(&gw.dev) {
-                        if let Some(addresses) = &iface.addresses {
-                            if let Some(addr) = addresses.iter().find(|addr| addr.is_ipv6()) {
-                                this.ifname = iface.name.clone();
-                                this.gateway = gw.gateway;
-                                this.address = addr.clone();
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
-        this
-    }
-
-    fn construct_fqdn(network: &NetworkInfo, default_hostname: &str) -> Fqdn {
-        let hostname = network.hostname.as_deref().unwrap_or(default_hostname);
-
-        let domain = network
-            .dns
-            .domain
-            .as_deref()
-            .unwrap_or(Self::DEFAULT_DOMAIN);
-
-        Fqdn::from(&format!("{hostname}.{domain}")).unwrap_or_else(|_| {
-            // Safety: This will always result in a valid FQDN, as we control & know
-            // the values of default_hostname (one of "pve", "pmg" or "pbs") and
-            // constant-defined DEFAULT_DOMAIN.
-            Fqdn::from(&format!("{}.{}", default_hostname, Self::DEFAULT_DOMAIN)).unwrap()
-        })
-    }
-}
-
 #[derive(Clone, Debug)]
 pub struct InstallerOptions {
     pub bootdisk: BootdiskOptions,
@@ -447,11 +68,15 @@ impl InstallerOptions {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::setup::{
-        Dns, Gateway, Interface, InterfaceState, IsoInfo, IsoLocations, NetworkInfo, ProductConfig,
-        ProxmoxProduct, Routes, SetupInfo,
+    use proxmox_installer_common::{
+        setup::{
+            Dns, Gateway, Interface, InterfaceState, IsoInfo, IsoLocations, NetworkInfo, ProductConfig,
+            ProxmoxProduct, Routes, SetupInfo,
+        },
+        utils::{CidrAddress, Fqdn},
     };
     use std::{collections::HashMap, path::PathBuf};
+    use std::net::{IpAddr, Ipv4Addr};
 
     fn dummy_setup_info() -> SetupInfo {
         SetupInfo {
diff --git a/proxmox-tui-installer/src/setup.rs b/proxmox-tui-installer/src/setup.rs
index 5575759..211a96b 100644
--- a/proxmox-tui-installer/src/setup.rs
+++ b/proxmox-tui-installer/src/setup.rs
@@ -1,131 +1,20 @@
 use std::{
-    cmp,
     collections::HashMap,
     fmt,
     fs::File,
     io::BufReader,
     net::IpAddr,
-    path::{Path, PathBuf},
+    path::Path,
 };
 
-use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
+use serde::{Deserialize, Serialize, Serializer};
 
-use crate::{
-    options::{
-        AdvancedBootdiskOptions, BtrfsRaidLevel, Disk, FsType, InstallerOptions,
-        ZfsBootdiskOptions, ZfsChecksumOption, ZfsCompressOption, ZfsRaidLevel,
-    },
-    utils::CidrAddress,
-};
-
-#[allow(clippy::upper_case_acronyms)]
-#[derive(Clone, Copy, Deserialize, PartialEq)]
-#[serde(rename_all = "lowercase")]
-pub enum ProxmoxProduct {
-    PVE,
-    PBS,
-    PMG,
-}
-
-impl ProxmoxProduct {
-    pub fn default_hostname(self) -> &'static str {
-        match self {
-            Self::PVE => "pve",
-            Self::PMG => "pmg",
-            Self::PBS => "pbs",
-        }
-    }
-}
-
-#[derive(Clone, Deserialize)]
-pub struct ProductConfig {
-    pub fullname: String,
-    pub product: ProxmoxProduct,
-    #[serde(deserialize_with = "deserialize_bool_from_int")]
-    pub enable_btrfs: bool,
-}
-
-#[derive(Clone, Deserialize)]
-pub struct IsoInfo {
-    pub release: String,
-    pub isorelease: String,
-}
-
-/// Paths in the ISO environment containing installer data.
-#[derive(Clone, Deserialize)]
-pub struct IsoLocations {
-    pub iso: PathBuf,
-}
-
-#[derive(Clone, Deserialize)]
-pub struct SetupInfo {
-    #[serde(rename = "product-cfg")]
-    pub config: ProductConfig,
-    #[serde(rename = "iso-info")]
-    pub iso_info: IsoInfo,
-    pub locations: IsoLocations,
-}
-
-#[derive(Clone, Deserialize)]
-pub struct CountryInfo {
-    pub name: String,
-    #[serde(default)]
-    pub zone: String,
-    pub kmap: String,
-}
-
-#[derive(Clone, Deserialize, Eq, PartialEq)]
-pub struct KeyboardMapping {
-    pub name: String,
-    #[serde(rename = "kvm")]
-    pub id: String,
-    #[serde(rename = "x11")]
-    pub xkb_layout: String,
-    #[serde(rename = "x11var")]
-    pub xkb_variant: String,
-}
-
-impl cmp::PartialOrd for KeyboardMapping {
-    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
-        self.name.partial_cmp(&other.name)
-    }
-}
-
-impl cmp::Ord for KeyboardMapping {
-    fn cmp(&self, other: &Self) -> cmp::Ordering {
-        self.name.cmp(&other.name)
-    }
-}
-
-#[derive(Clone, Deserialize)]
-pub struct LocaleInfo {
-    #[serde(deserialize_with = "deserialize_cczones_map")]
-    pub cczones: HashMap<String, Vec<String>>,
-    #[serde(rename = "country")]
-    pub countries: HashMap<String, CountryInfo>,
-    pub kmap: HashMap<String, KeyboardMapping>,
-}
-
-#[derive(Serialize)]
-struct InstallZfsOption {
-    ashift: usize,
-    #[serde(serialize_with = "serialize_as_display")]
-    compress: ZfsCompressOption,
-    #[serde(serialize_with = "serialize_as_display")]
-    checksum: ZfsChecksumOption,
-    copies: usize,
-}
-
-impl From<ZfsBootdiskOptions> for InstallZfsOption {
-    fn from(opts: ZfsBootdiskOptions) -> Self {
-        InstallZfsOption {
-            ashift: opts.ashift,
-            compress: opts.compress,
-            checksum: opts.checksum,
-            copies: opts.copies,
-        }
-    }
-}
+use crate::options::InstallerOptions;
+use proxmox_installer_common::{
+        options::{AdvancedBootdiskOptions, BtrfsRaidLevel, Disk, FsType, ZfsRaidLevel},
+        setup::InstallZfsOption,
+        utils::CidrAddress,
+    };
 
 /// See Proxmox::Install::Config
 #[derive(Serialize)]
@@ -247,82 +136,6 @@ pub fn read_json<T: for<'de> Deserialize<'de>, P: AsRef<Path>>(path: P) -> Resul
     serde_json::from_reader(reader).map_err(|err| format!("failed to parse JSON: {err}"))
 }
 
-fn deserialize_bool_from_int<'de, D>(deserializer: D) -> Result<bool, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    let val: u32 = Deserialize::deserialize(deserializer)?;
-    Ok(val != 0)
-}
-
-fn deserialize_cczones_map<'de, D>(
-    deserializer: D,
-) -> Result<HashMap<String, Vec<String>>, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    let map: HashMap<String, HashMap<String, u32>> = Deserialize::deserialize(deserializer)?;
-
-    let mut result = HashMap::new();
-    for (cc, list) in map.into_iter() {
-        result.insert(cc, list.into_keys().collect());
-    }
-
-    Ok(result)
-}
-
-fn deserialize_disks_map<'de, D>(deserializer: D) -> Result<Vec<Disk>, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    let disks =
-        <Vec<(usize, String, f64, String, Option<usize>, String)>>::deserialize(deserializer)?;
-    Ok(disks
-        .into_iter()
-        .map(
-            |(index, device, size_mb, model, logical_bsize, _syspath)| Disk {
-                index: index.to_string(),
-                // Linux always reports the size of block devices in sectors, where one sector is
-                // defined as being 2^9 = 512 bytes in size.
-                // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/blk_types.h?h=v6.4#n30
-                size: (size_mb * 512.) / 1024. / 1024. / 1024.,
-                block_size: logical_bsize,
-                path: device,
-                model: (!model.is_empty()).then_some(model),
-            },
-        )
-        .collect())
-}
-
-fn deserialize_cidr_list<'de, D>(deserializer: D) -> Result<Option<Vec<CidrAddress>>, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    #[derive(Deserialize)]
-    struct CidrDescriptor {
-        address: String,
-        prefix: usize,
-        // family is implied anyway by parsing the address
-    }
-
-    let list: Vec<CidrDescriptor> = Deserialize::deserialize(deserializer)?;
-
-    let mut result = Vec::with_capacity(list.len());
-    for desc in list {
-        let ip_addr = desc
-            .address
-            .parse::<IpAddr>()
-            .map_err(|err| de::Error::custom(format!("{:?}", err)))?;
-
-        result.push(
-            CidrAddress::new(ip_addr, desc.prefix)
-                .map_err(|err| de::Error::custom(format!("{:?}", err)))?,
-        );
-    }
-
-    Ok(Some(result))
-}
-
 fn serialize_disk_opt<S>(value: &Option<Disk>, serializer: S) -> Result<S::Ok, S::Error>
 where
     S: Serializer,
@@ -366,116 +179,3 @@ where
 {
     serializer.collect_str(value)
 }
-
-#[derive(Clone, Deserialize)]
-pub struct RuntimeInfo {
-    /// Whether is system was booted in (legacy) BIOS or UEFI mode.
-    pub boot_type: BootType,
-
-    /// Detected country if available.
-    pub country: Option<String>,
-
-    /// Maps devices to their information.
-    #[serde(deserialize_with = "deserialize_disks_map")]
-    pub disks: Vec<Disk>,
-
-    /// Network addresses, gateways and DNS info.
-    pub network: NetworkInfo,
-
-    /// Total memory of the system in MiB.
-    pub total_memory: usize,
-
-    /// Whether the CPU supports hardware-accelerated virtualization
-    #[serde(deserialize_with = "deserialize_bool_from_int")]
-    pub hvm_supported: bool,
-}
-
-#[derive(Copy, Clone, Eq, Deserialize, PartialEq)]
-#[serde(rename_all = "lowercase")]
-pub enum BootType {
-    Bios,
-    Efi,
-}
-
-#[derive(Clone, Deserialize)]
-pub struct NetworkInfo {
-    pub dns: Dns,
-    pub routes: Option<Routes>,
-
-    /// Maps devices to their configuration, if it has a usable configuration.
-    /// (Contains no entries for devices with only link-local addresses.)
-    #[serde(default)]
-    pub interfaces: HashMap<String, Interface>,
-
-    /// The hostname of this machine, if set by the DHCP server.
-    pub hostname: Option<String>,
-}
-
-#[derive(Clone, Deserialize)]
-pub struct Dns {
-    pub domain: Option<String>,
-
-    /// List of stringified IP addresses.
-    #[serde(default)]
-    pub dns: Vec<IpAddr>,
-}
-
-#[derive(Clone, Deserialize)]
-pub struct Routes {
-    /// Ipv4 gateway.
-    pub gateway4: Option<Gateway>,
-
-    /// Ipv6 gateway.
-    pub gateway6: Option<Gateway>,
-}
-
-#[derive(Clone, Deserialize)]
-pub struct Gateway {
-    /// Outgoing network device.
-    pub dev: String,
-
-    /// Stringified gateway IP address.
-    pub gateway: IpAddr,
-}
-
-#[derive(Clone, Deserialize)]
-#[serde(rename_all = "UPPERCASE")]
-pub enum InterfaceState {
-    Up,
-    Down,
-    #[serde(other)]
-    Unknown,
-}
-
-impl InterfaceState {
-    // avoid display trait as this is not the string representation for a serializer
-    pub fn render(&self) -> String {
-        match self {
-            Self::Up => "\u{25CF}",
-            Self::Down | Self::Unknown => " ",
-        }
-        .into()
-    }
-}
-
-#[derive(Clone, Deserialize)]
-pub struct Interface {
-    pub name: String,
-
-    pub index: usize,
-
-    pub mac: String,
-
-    pub state: InterfaceState,
-
-    #[serde(default)]
-    #[serde(deserialize_with = "deserialize_cidr_list")]
-    pub addresses: Option<Vec<CidrAddress>>,
-}
-
-impl Interface {
-    // avoid display trait as this is not the string representation for a serializer
-    pub fn render(&self) -> String {
-        format!("{} {}", self.state.render(), self.name)
-    }
-}
diff --git a/proxmox-tui-installer/src/system.rs b/proxmox-tui-installer/src/system.rs
index bbf13b8..d1675a9 100644
--- a/proxmox-tui-installer/src/system.rs
+++ b/proxmox-tui-installer/src/system.rs
@@ -1,6 +1,6 @@
 use std::{fs::OpenOptions, io::Write, process::Command};
 
-use crate::setup::KeyboardMapping;
+use proxmox_installer_common::setup::KeyboardMapping;
 
 pub fn set_keyboard_layout(kmap: &KeyboardMapping) -> Result<(), String> {
     Command::new("setxkbmap")
diff --git a/proxmox-tui-installer/src/views/bootdisk.rs b/proxmox-tui-installer/src/views/bootdisk.rs
index ba08c8b..38a6521 100644
--- a/proxmox-tui-installer/src/views/bootdisk.rs
+++ b/proxmox-tui-installer/src/views/bootdisk.rs
@@ -1,4 +1,4 @@
-use std::{cell::RefCell, collections::HashSet, marker::PhantomData, rc::Rc};
+use std::{cell::RefCell, marker::PhantomData, rc::Rc};
 
 use cursive::{
     view::{Nameable, Resizable, ViewWrapper},
@@ -10,15 +10,18 @@ use cursive::{
 };
 
 use super::{DiskSizeEditView, FormView, IntegerEditView};
-use crate::{
+use crate::options::FS_TYPES;
+use crate::InstallerState;
+
+use proxmox_installer_common::{
     options::{
-        AdvancedBootdiskOptions, BootdiskOptions, BtrfsBootdiskOptions, BtrfsRaidLevel, Disk,
-        FsType, LvmBootdiskOptions, ZfsBootdiskOptions, ZfsRaidLevel, FS_TYPES,
+        AdvancedBootdiskOptions, BootdiskOptions, BtrfsBootdiskOptions, Disk,
+        FsType, LvmBootdiskOptions, ZfsBootdiskOptions,
         ZFS_CHECKSUM_OPTIONS, ZFS_COMPRESS_OPTIONS,
     },
-    setup::{BootType, ProductConfig},
+    setup::{BootType, ProductConfig, ProxmoxProduct},
+    disk_checks::{check_btrfs_raid_config, check_for_duplicate_disks, check_disks_4kn_legacy_boot, check_zfs_raid_config},
 };
-use crate::{setup::ProxmoxProduct, InstallerState};
 
 pub struct BootdiskOptionsView {
     view: LinearLayout,
@@ -619,236 +622,3 @@ fn advanced_options_view(
     .with_name("advanced-bootdisk-options-dialog")
     .max_size((120, 40))
 }
-
-/// Checks a list of disks for duplicate entries, using their index as key.
-///
-/// # Arguments
-///
-/// * `disks` - A list of disks to check for duplicates.
-fn check_for_duplicate_disks(disks: &[Disk]) -> Result<(), &Disk> {
-    let mut set = HashSet::new();
-
-    for disk in disks {
-        if !set.insert(&disk.index) {
-            return Err(disk);
-        }
-    }
-
-    Ok(())
-}
-
-/// Simple wrapper which returns an descriptive error if the list of disks is too short.
-///
-/// # Arguments
-///
-/// * `disks` - A list of disks to check the lenght of.
-/// * `min` - Minimum number of disks
-fn check_raid_min_disks(disks: &[Disk], min: usize) -> Result<(), String> {
-    if disks.len() < min {
-        Err(format!("Need at least {min} disks"))
-    } else {
-        Ok(())
-    }
-}
-
-/// Checks all disks for legacy BIOS boot compatibility and reports an error as appropriate. 4Kn
-/// disks are generally broken with legacy BIOS and cannot be booted from.
-///
-/// # Arguments
-///
-/// * `runinfo` - `RuntimeInfo` instance of currently running system
-/// * `disks` - List of disks designated as bootdisk targets.
-fn check_disks_4kn_legacy_boot(boot_type: BootType, disks: &[Disk]) -> Result<(), &str> {
-    let is_blocksize_4096 = |disk: &Disk| disk.block_size.map(|s| s == 4096).unwrap_or(false);
-
-    if boot_type == BootType::Bios && disks.iter().any(is_blocksize_4096) {
-        return Err("Booting from 4Kn drive in legacy BIOS mode is not supported.");
-    }
-
-    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.
-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)?;
-            // 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.
-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 super::*;
-
-    fn dummy_disk(index: usize) -> Disk {
-        Disk {
-            index: index.to_string(),
-            path: format!("/dev/dummy{index}"),
-            model: Some("Dummy disk".to_owned()),
-            size: 1024. * 1024. * 1024. * 8.,
-            block_size: Some(512),
-        }
-    }
-
-    fn dummy_disks(num: usize) -> Vec<Disk> {
-        (0..num).map(dummy_disk).collect()
-    }
-
-    #[test]
-    fn duplicate_disks() {
-        assert!(check_for_duplicate_disks(&dummy_disks(2)).is_ok());
-        assert_eq!(
-            check_for_duplicate_disks(&[
-                dummy_disk(0),
-                dummy_disk(1),
-                dummy_disk(2),
-                dummy_disk(2),
-                dummy_disk(3),
-            ]),
-            Err(&dummy_disk(2)),
-        );
-    }
-
-    #[test]
-    fn raid_min_disks() {
-        let disks = dummy_disks(10);
-
-        assert!(check_raid_min_disks(&disks[..1], 2).is_err());
-        assert!(check_raid_min_disks(&disks[..1], 1).is_ok());
-        assert!(check_raid_min_disks(&disks, 1).is_ok());
-    }
-
-    #[test]
-    fn bios_boot_compat_4kn() {
-        for i in 0..10 {
-            let mut disks = dummy_disks(10);
-            disks[i].block_size = Some(4096);
-
-            // Must fail if /any/ of the disks are 4Kn
-            assert!(check_disks_4kn_legacy_boot(BootType::Bios, &disks).is_err());
-            // For UEFI, we allow it for every configuration
-            assert!(check_disks_4kn_legacy_boot(BootType::Efi, &disks).is_ok());
-        }
-    }
-
-    #[test]
-    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());
-    }
-
-    #[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());
-    }
-}
diff --git a/proxmox-tui-installer/src/views/mod.rs b/proxmox-tui-installer/src/views/mod.rs
index aa24fa4..aabae0e 100644
--- a/proxmox-tui-installer/src/views/mod.rs
+++ b/proxmox-tui-installer/src/views/mod.rs
@@ -7,7 +7,7 @@ use cursive::{
     Rect, Vec2, View,
 };
 
-use crate::utils::CidrAddress;
+use proxmox_installer_common::utils::CidrAddress;
 
 mod bootdisk;
 pub use bootdisk::*;
diff --git a/proxmox-tui-installer/src/views/timezone.rs b/proxmox-tui-installer/src/views/timezone.rs
index 6732286..77fbb10 100644
--- a/proxmox-tui-installer/src/views/timezone.rs
+++ b/proxmox-tui-installer/src/views/timezone.rs
@@ -6,9 +6,11 @@ use cursive::{
 
 use super::FormView;
 use crate::{
+    system, InstallerState,
+};
+use proxmox_installer_common::{
     options::TimezoneOptions,
     setup::{KeyboardMapping, LocaleInfo},
-    system, InstallerState,
 };
 
 pub struct TimezoneOptionsView {
-- 
2.39.2






More information about the pve-devel mailing list