From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id B638C1FF13C for ; Thu, 30 Apr 2026 14:50:34 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 8131A7C3F; Thu, 30 Apr 2026 14:50:34 +0200 (CEST) From: Christoph Heiss To: pdm-devel@lists.proxmox.com Subject: [PATCH installer v4 33/40] tree-wide: switch to filesystem types from proxmox-installer-types Date: Thu, 30 Apr 2026 14:47:02 +0200 Message-ID: <20260430124712.1614305-34-c.heiss@proxmox.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260430124712.1614305-1-c.heiss@proxmox.com> References: <20260430124712.1614305-1-c.heiss@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1777553319289 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.075 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: LX6OYCI4BPMI5WSS5TXUWBVZDIWBKDBY X-Message-ID-Hash: LX6OYCI4BPMI5WSS5TXUWBVZDIWBKDBY X-MailFrom: c.heiss@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: No functional changes. Reviewed-by: Lukas Wagner Signed-off-by: Christoph Heiss --- Changes v3 -> v4: * no changes Changes v2 -> v3: * new patch Cargo.toml | 2 + proxmox-auto-installer/Cargo.toml | 1 + proxmox-auto-installer/src/answer.rs | 26 +- proxmox-auto-installer/src/utils.rs | 16 +- proxmox-chroot/Cargo.toml | 1 + proxmox-chroot/src/main.rs | 60 +- proxmox-installer-common/Cargo.toml | 1 + proxmox-installer-common/src/dmi.rs | 43 ++ proxmox-installer-common/src/lib.rs | 1 + proxmox-installer-common/src/options.rs | 197 ++---- proxmox-installer-common/src/setup.rs | 5 +- proxmox-post-hook/Cargo.toml | 3 +- proxmox-post-hook/src/main.rs | 688 ++++++++------------ proxmox-tui-installer/Cargo.toml | 1 + proxmox-tui-installer/src/options.rs | 21 +- proxmox-tui-installer/src/views/bootdisk.rs | 30 +- 16 files changed, 426 insertions(+), 670 deletions(-) create mode 100644 proxmox-installer-common/src/dmi.rs diff --git a/Cargo.toml b/Cargo.toml index 379ee6b..2466822 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,8 +28,10 @@ toml = "0.8" proxmox-auto-installer.path = "./proxmox-auto-installer" proxmox-installer-common.path = "./proxmox-installer-common" proxmox-network-types = "1.0" +proxmox-installer-types = { version = "0.1", features = ["legacy"] } # Local path overrides # NOTE: You must run `cargo update` after changing this for it to take effect! [patch.crates-io] # proxmox-network-types.path = "../proxmox/proxmox-network-types" +# proxmox-installer-types.path = "../proxmox/proxmox-installer-types" diff --git a/proxmox-auto-installer/Cargo.toml b/proxmox-auto-installer/Cargo.toml index 0086e5d..5ef2f4f 100644 --- a/proxmox-auto-installer/Cargo.toml +++ b/proxmox-auto-installer/Cargo.toml @@ -15,6 +15,7 @@ anyhow.workspace = true log.workspace = true proxmox-installer-common = { workspace = true, features = ["http"] } proxmox-network-types.workspace = true +proxmox-installer-types.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true serde_plain.workspace = true diff --git a/proxmox-auto-installer/src/answer.rs b/proxmox-auto-installer/src/answer.rs index acb0d5b..eec5b58 100644 --- a/proxmox-auto-installer/src/answer.rs +++ b/proxmox-auto-installer/src/answer.rs @@ -7,9 +7,9 @@ use std::{ }; use proxmox_installer_common::options::{ - BtrfsCompressOption, BtrfsRaidLevel, FsType, NetworkInterfacePinningOptions, ZfsChecksumOption, - ZfsCompressOption, ZfsRaidLevel, + BtrfsCompressOption, NetworkInterfacePinningOptions, ZfsChecksumOption, ZfsCompressOption, }; +use proxmox_installer_types::answer::{BtrfsRaidLevel, FilesystemType, ZfsRaidLevel}; use proxmox_network_types::{Cidr, fqdn::Fqdn}; // NOTE New answer file properties must use kebab-case, but should allow snake_case for backwards @@ -314,7 +314,7 @@ pub struct DiskSetup { #[derive(Clone, Debug, Deserialize)] #[serde(try_from = "DiskSetup", deny_unknown_fields)] pub struct Disks { - pub fs_type: FsType, + pub fs_type: FilesystemType, pub disk_selection: DiskSelection, pub filter_match: Option, pub fs_options: FsOptions, @@ -351,11 +351,17 @@ impl TryFrom for Disks { let (fs, fs_options) = match source.filesystem { Filesystem::Xfs => { lvm_checks(&source)?; - (FsType::Xfs, FsOptions::LVM(source.lvm.unwrap_or_default())) + ( + FilesystemType::Xfs, + FsOptions::LVM(source.lvm.unwrap_or_default()), + ) } Filesystem::Ext4 => { lvm_checks(&source)?; - (FsType::Ext4, FsOptions::LVM(source.lvm.unwrap_or_default())) + ( + FilesystemType::Ext4, + FsOptions::LVM(source.lvm.unwrap_or_default()), + ) } Filesystem::Zfs => { if source.lvm.is_some() || source.btrfs.is_some() { @@ -365,7 +371,10 @@ impl TryFrom for Disks { None | Some(ZfsOptions { raid: None, .. }) => { return Err("ZFS raid level 'zfs.raid' must be set"); } - Some(opts) => (FsType::Zfs(opts.raid.unwrap()), FsOptions::ZFS(opts)), + Some(opts) => ( + FilesystemType::Zfs(opts.raid.unwrap()), + FsOptions::ZFS(opts), + ), } } Filesystem::Btrfs => { @@ -376,7 +385,10 @@ impl TryFrom for Disks { None | Some(BtrfsOptions { raid: None, .. }) => { return Err("BTRFS raid level 'btrfs.raid' must be set"); } - Some(opts) => (FsType::Btrfs(opts.raid.unwrap()), FsOptions::BTRFS(opts)), + Some(opts) => ( + FilesystemType::Btrfs(opts.raid.unwrap()), + FsOptions::BTRFS(opts), + ), } } }; diff --git a/proxmox-auto-installer/src/utils.rs b/proxmox-auto-installer/src/utils.rs index f9cfcdd..83be913 100644 --- a/proxmox-auto-installer/src/utils.rs +++ b/proxmox-auto-installer/src/utils.rs @@ -16,12 +16,13 @@ use crate::{ use proxmox_installer_common::{ ROOT_PASSWORD_MIN_LENGTH, disk_checks::check_swapsize, - options::{FsType, NetworkOptions, ZfsChecksumOption, ZfsCompressOption, email_validate}, + options::{NetworkOptions, RaidLevel, ZfsChecksumOption, ZfsCompressOption, email_validate}, setup::{ InstallBtrfsOption, InstallConfig, InstallFirstBootSetup, InstallRootPassword, InstallZfsOption, LocaleInfo, RuntimeInfo, SetupInfo, }, }; +use proxmox_installer_types::answer::FilesystemType; use serde::{Deserialize, Serialize}; fn get_network_settings( @@ -211,8 +212,10 @@ fn set_disks( config: &mut InstallConfig, ) -> Result<()> { match config.filesys { - FsType::Ext4 | FsType::Xfs => set_single_disk(answer, udev_info, runtime_info, config), - FsType::Zfs(_) | FsType::Btrfs(_) => { + FilesystemType::Ext4 | FilesystemType::Xfs => { + set_single_disk(answer, udev_info, runtime_info, config) + } + FilesystemType::Zfs(_) | FilesystemType::Btrfs(_) => { set_selected_disks(answer, udev_info, runtime_info, config) } } @@ -410,7 +413,12 @@ 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(); + let min_disks = match answer.disks.fs_type { + FilesystemType::Ext4 | FilesystemType::Xfs => 1, + FilesystemType::Zfs(level) => level.get_min_disks(), + FilesystemType::Btrfs(level) => level.get_min_disks(), + }; + if selection.len() < min_disks { bail!( "{}: need at least {} disks", diff --git a/proxmox-chroot/Cargo.toml b/proxmox-chroot/Cargo.toml index a6a705d..e1e0e4c 100644 --- a/proxmox-chroot/Cargo.toml +++ b/proxmox-chroot/Cargo.toml @@ -10,5 +10,6 @@ homepage = "https://www.proxmox.com" [dependencies] anyhow.workspace = true proxmox-installer-common = { workspace = true, features = [ "cli" ] } +proxmox-installer-types.workspace = true serde = { workspace = true, features = [ "derive" ] } serde_json.workspace = true diff --git a/proxmox-chroot/src/main.rs b/proxmox-chroot/src/main.rs index 2cff630..5f087bb 100644 --- a/proxmox-chroot/src/main.rs +++ b/proxmox-chroot/src/main.rs @@ -5,20 +5,19 @@ #![forbid(unsafe_code)] +use anyhow::{Result, bail}; +use serde::Deserialize; use std::{ env, fs, io, path::{self, Path, PathBuf}, process::{self, Command}, - str::FromStr, }; -use anyhow::{Result, bail}; use proxmox_installer_common::{ RUNTIME_DIR, cli, - options::FsType, setup::{InstallConfig, SetupInfo}, }; -use serde::Deserialize; +use proxmox_installer_types::answer::Filesystem; const ANSWER_MP: &str = "answer"; static BINDMOUNTS: [&str; 4] = ["dev", "proc", "run", "sys"]; @@ -29,7 +28,7 @@ const ZPOOL_NAME: &str = "rpool"; struct CommandPrepareArgs { /// Filesystem used for the installation. Will try to automatically detect it after a /// successful installation. - filesystem: Option, + filesystem: Option, /// Numerical ID of the `rpool` ZFS pool to import. Needed if multiple pools of name `rpool` /// are present. @@ -74,7 +73,7 @@ OPTIONS: /// Arguments for the `cleanup` command. struct CommandCleanupArgs { /// Filesystem used for the installation. Will try to automatically detect it by default. - filesystem: Option, + filesystem: Option, } impl cli::Subcommand for CommandCleanupArgs { @@ -105,39 +104,6 @@ OPTIONS: } } -#[derive(Copy, Clone, Debug)] -enum Filesystems { - Zfs, - Ext4, - Xfs, - Btrfs, -} - -impl From for Filesystems { - fn from(fs: FsType) -> Self { - match fs { - FsType::Xfs => Self::Xfs, - FsType::Ext4 => Self::Ext4, - FsType::Zfs(_) => Self::Zfs, - FsType::Btrfs(_) => Self::Btrfs, - } - } -} - -impl FromStr for Filesystems { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s { - "ext4" => Ok(Filesystems::Ext4), - "xfs" => Ok(Filesystems::Xfs), - _ if s.starts_with("zfs") => Ok(Filesystems::Zfs), - _ if s.starts_with("btrfs") => Ok(Filesystems::Btrfs), - _ => bail!("unknown filesystem"), - } - } -} - fn main() -> process::ExitCode { cli::run(cli::AppInfo { global_help: &format!( @@ -171,10 +137,10 @@ fn prepare(args: &CommandPrepareArgs) -> Result<()> { fs::create_dir_all(TARGET_DIR)?; match fs { - Filesystems::Zfs => mount_zpool(args.rpool_id)?, - Filesystems::Xfs => mount_fs()?, - Filesystems::Ext4 => mount_fs()?, - Filesystems::Btrfs => mount_btrfs(args.btrfs_uuid.clone())?, + Filesystem::Zfs => mount_zpool(args.rpool_id)?, + Filesystem::Xfs => mount_fs()?, + Filesystem::Ext4 => mount_fs()?, + Filesystem::Btrfs => mount_btrfs(args.btrfs_uuid.clone())?, } if let Err(e) = bindmount() { @@ -193,15 +159,15 @@ fn cleanup(args: &CommandCleanupArgs) -> Result<()> { } match fs { - Filesystems::Zfs => umount_zpool(), - Filesystems::Btrfs | Filesystems::Xfs | Filesystems::Ext4 => umount(Path::new(TARGET_DIR))?, + Filesystem::Zfs => umount_zpool(), + Filesystem::Btrfs | Filesystem::Xfs | Filesystem::Ext4 => umount(Path::new(TARGET_DIR))?, } println!("Chroot cleanup done. You can now reboot or leave the shell."); Ok(()) } -fn get_fs(filesystem: Option) -> Result { +fn get_fs(filesystem: Option) -> Result { let fs = match filesystem { None => { let low_level_config = match get_low_level_config() { @@ -210,7 +176,7 @@ fn get_fs(filesystem: Option) -> Result { "Could not fetch config from previous installation. Please specify file system with -f." ), }; - Filesystems::from(low_level_config.filesys) + low_level_config.filesys.into() } Some(fs) => fs, }; diff --git a/proxmox-installer-common/Cargo.toml b/proxmox-installer-common/Cargo.toml index 7469627..7682680 100644 --- a/proxmox-installer-common/Cargo.toml +++ b/proxmox-installer-common/Cargo.toml @@ -14,6 +14,7 @@ serde = { workspace = true, features = [ "derive" ] } serde_json.workspace = true serde_plain.workspace = true proxmox-network-types.workspace = true +proxmox-installer-types.workspace = true # `http` feature hex = { version = "0.4", optional = true } diff --git a/proxmox-installer-common/src/dmi.rs b/proxmox-installer-common/src/dmi.rs new file mode 100644 index 0000000..76ae4a5 --- /dev/null +++ b/proxmox-installer-common/src/dmi.rs @@ -0,0 +1,43 @@ +use std::{collections::HashMap, fs}; + +use anyhow::{Result, bail}; +use proxmox_installer_types::SystemDMI; + +const DMI_PATH: &str = "/sys/devices/virtual/dmi/id"; + +pub fn get() -> Result { + let system_files = [ + "product_serial", + "product_sku", + "product_uuid", + "product_name", + ]; + let baseboard_files = ["board_asset_tag", "board_serial", "board_name"]; + let chassis_files = ["chassis_serial", "chassis_sku", "chassis_asset_tag"]; + + Ok(SystemDMI { + system: get_dmi_infos_for(&system_files)?, + baseboard: get_dmi_infos_for(&baseboard_files)?, + chassis: get_dmi_infos_for(&chassis_files)?, + }) +} + +fn get_dmi_infos_for(files: &[&str]) -> Result> { + let mut res: HashMap = HashMap::new(); + + for file in files { + let path = format!("{DMI_PATH}/{file}"); + let content = match fs::read_to_string(&path) { + Err(ref err) if err.kind() == std::io::ErrorKind::NotFound => continue, + Err(ref err) if err.kind() == std::io::ErrorKind::PermissionDenied => { + bail!("Could not read data. Are you running as root or with sudo?") + } + Err(err) => bail!("Error: '{err}' on '{path}'"), + Ok(content) => content.trim().into(), + }; + let key = file.splitn(2, '_').last().unwrap(); + res.insert(key.into(), content); + } + + Ok(res) +} diff --git a/proxmox-installer-common/src/lib.rs b/proxmox-installer-common/src/lib.rs index 7cdb1de..05445d5 100644 --- a/proxmox-installer-common/src/lib.rs +++ b/proxmox-installer-common/src/lib.rs @@ -1,4 +1,5 @@ pub mod disk_checks; +pub mod dmi; pub mod options; pub mod setup; pub mod sysinfo; diff --git a/proxmox-installer-common/src/options.rs b/proxmox-installer-common/src/options.rs index f903f7e..8e19663 100644 --- a/proxmox-installer-common/src/options.rs +++ b/proxmox-installer-common/src/options.rs @@ -1,36 +1,23 @@ use anyhow::{Result, bail}; use regex::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::net::{IpAddr, Ipv4Addr}; -use std::str::FromStr; -use std::sync::OnceLock; -use std::{cmp, fmt}; +use std::{ + cmp, + collections::HashMap, + fmt, + net::{IpAddr, Ipv4Addr}, + sync::OnceLock, +}; use crate::disk_checks::check_raid_min_disks; use crate::net::{MAX_IFNAME_LEN, MIN_IFNAME_LEN}; use crate::setup::{LocaleInfo, NetworkInfo, RuntimeInfo, SetupInfo}; +use proxmox_installer_types::answer::{BtrfsRaidLevel, FilesystemType, ZfsRaidLevel}; use proxmox_network_types::{fqdn::Fqdn, ip_address::Cidr}; -#[derive(Copy, Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] -#[serde(rename_all(deserialize = "lowercase", serialize = "UPPERCASE"))] -pub enum BtrfsRaidLevel { - #[serde(alias = "RAID0")] - Raid0, - #[serde(alias = "RAID1")] - Raid1, - #[serde(alias = "RAID10")] - Raid10, -} - -impl BtrfsRaidLevel { - pub fn get_min_disks(&self) -> usize { - match self { - BtrfsRaidLevel::Raid0 => 1, - BtrfsRaidLevel::Raid1 => 2, - BtrfsRaidLevel::Raid10 => 4, - } - } +pub trait RaidLevel { + /// Returns the minimum number of disks needed for this RAID level. + fn get_min_disks(&self) -> usize; /// Checks whether a user-supplied Btrfs RAID setup is valid or not, such as minimum /// number of disks. @@ -38,42 +25,31 @@ impl BtrfsRaidLevel { /// # Arguments /// /// * `disks` - List of disks designated as RAID targets. - pub fn check_raid_disks_setup(&self, disks: &[Disk]) -> Result<(), String> { + fn check_raid_disks_setup(&self, disks: &[Disk]) -> Result<(), String>; + + /// Checks whether the given disk sizes are compatible for the RAID level, if it is a mirror. + fn check_mirror_size(&self, _disk1: &Disk, _disk2: &Disk) -> Result<(), String> { + Ok(()) + } +} + +impl RaidLevel for BtrfsRaidLevel { + fn get_min_disks(&self) -> usize { + match self { + Self::Raid0 => 1, + Self::Raid1 => 2, + Self::Raid10 => 4, + } + } + + fn check_raid_disks_setup(&self, disks: &[Disk]) -> Result<(), String> { check_raid_min_disks(disks, self.get_min_disks())?; Ok(()) } } -serde_plain::derive_display_from_serialize!(BtrfsRaidLevel); - -#[derive(Copy, Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] -#[serde(rename_all(deserialize = "lowercase", serialize = "UPPERCASE"))] -pub enum ZfsRaidLevel { - #[serde(alias = "RAID0")] - Raid0, - #[serde(alias = "RAID1")] - Raid1, - #[serde(alias = "RAID10")] - Raid10, - #[serde( - alias = "RAIDZ-1", - rename(deserialize = "raidz-1", serialize = "RAIDZ-1") - )] - RaidZ, - #[serde( - alias = "RAIDZ-2", - rename(deserialize = "raidz-2", serialize = "RAIDZ-2") - )] - RaidZ2, - #[serde( - alias = "RAIDZ-3", - rename(deserialize = "raidz-3", serialize = "RAIDZ-3") - )] - RaidZ3, -} - -impl ZfsRaidLevel { - pub fn get_min_disks(&self) -> usize { +impl RaidLevel for ZfsRaidLevel { + fn get_min_disks(&self) -> usize { match self { ZfsRaidLevel::Raid0 => 1, ZfsRaidLevel::Raid1 => 2, @@ -84,23 +60,7 @@ impl ZfsRaidLevel { } } - 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_raid_disks_setup(&self, disks: &[Disk]) -> Result<(), String> { + fn check_raid_disks_setup(&self, disks: &[Disk]) -> Result<(), String> { check_raid_min_disks(disks, self.get_min_disks())?; match self { @@ -130,93 +90,18 @@ impl ZfsRaidLevel { Ok(()) } -} -serde_plain::derive_display_from_serialize!(ZfsRaidLevel); - -#[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(_)) - } - - /// Returns true if the filesystem is used on top of LVM, e.g. ext4 or XFS. - 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(), + 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(()) } } } -impl fmt::Display for FsType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // Values displayed to the user in the installer UI - match self { - FsType::Ext4 => write!(f, "ext4"), - FsType::Xfs => write!(f, "XFS"), - FsType::Zfs(level) => write!(f, "ZFS ({level})"), - FsType::Btrfs(level) => write!(f, "BTRFS ({level})"), - } - } -} - -impl Serialize for FsType { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - // These values must match exactly what the low-level installer expects - let value = match self { - // proxinstall::$fssetup - FsType::Ext4 => "ext4", - FsType::Xfs => "xfs", - // proxinstall::get_zfs_raid_setup() - FsType::Zfs(level) => &format!("zfs ({level})"), - // proxinstall::get_btrfs_raid_setup() - FsType::Btrfs(level) => &format!("btrfs ({level})"), - }; - - serializer.collect_str(value) - } -} - -impl FromStr for FsType { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "ext4" => Ok(FsType::Ext4), - "xfs" => Ok(FsType::Xfs), - "zfs (RAID0)" => Ok(FsType::Zfs(ZfsRaidLevel::Raid0)), - "zfs (RAID1)" => Ok(FsType::Zfs(ZfsRaidLevel::Raid1)), - "zfs (RAID10)" => Ok(FsType::Zfs(ZfsRaidLevel::Raid10)), - "zfs (RAIDZ-1)" => Ok(FsType::Zfs(ZfsRaidLevel::RaidZ)), - "zfs (RAIDZ-2)" => Ok(FsType::Zfs(ZfsRaidLevel::RaidZ2)), - "zfs (RAIDZ-3)" => Ok(FsType::Zfs(ZfsRaidLevel::RaidZ3)), - "btrfs (RAID0)" => Ok(FsType::Btrfs(BtrfsRaidLevel::Raid0)), - "btrfs (RAID1)" => Ok(FsType::Btrfs(BtrfsRaidLevel::Raid1)), - "btrfs (RAID10)" => Ok(FsType::Btrfs(BtrfsRaidLevel::Raid10)), - _ => Err(format!("Could not find file system: {s}")), - } - } -} - -serde_plain::derive_deserialize_from_fromstr!(FsType, "valid filesystem"); - #[derive(Clone, Debug)] pub struct LvmBootdiskOptions { pub total_size: f64, @@ -426,7 +311,7 @@ impl cmp::Ord for Disk { #[derive(Clone, Debug)] pub struct BootdiskOptions { pub disks: Vec, - pub fstype: FsType, + pub fstype: FilesystemType, pub advanced: AdvancedBootdiskOptions, } @@ -434,7 +319,7 @@ impl BootdiskOptions { pub fn defaults_from(disk: &Disk) -> Self { Self { disks: vec![disk.clone()], - fstype: FsType::Ext4, + fstype: FilesystemType::Ext4, advanced: AdvancedBootdiskOptions::Lvm(LvmBootdiskOptions::defaults_from(disk)), } } diff --git a/proxmox-installer-common/src/setup.rs b/proxmox-installer-common/src/setup.rs index 35a5436..91f1250 100644 --- a/proxmox-installer-common/src/setup.rs +++ b/proxmox-installer-common/src/setup.rs @@ -14,9 +14,10 @@ use proxmox_network_types::Cidr; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use crate::options::{ - BtrfsBootdiskOptions, BtrfsCompressOption, Disk, FsType, NetworkInterfacePinningOptions, + BtrfsBootdiskOptions, BtrfsCompressOption, Disk, NetworkInterfacePinningOptions, ZfsBootdiskOptions, ZfsChecksumOption, ZfsCompressOption, }; +use proxmox_installer_types::answer::FilesystemType; #[allow(clippy::upper_case_acronyms)] #[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] @@ -565,7 +566,7 @@ pub fn spawn_low_level_installer(test_mode: bool) -> io::Result pub struct InstallConfig { pub autoreboot: usize, - pub filesys: FsType, + pub filesys: FilesystemType, pub hdsize: f64, #[serde(skip_serializing_if = "Option::is_none")] pub swapsize: Option, diff --git a/proxmox-post-hook/Cargo.toml b/proxmox-post-hook/Cargo.toml index beaaa26..748b922 100644 --- a/proxmox-post-hook/Cargo.toml +++ b/proxmox-post-hook/Cargo.toml @@ -12,8 +12,9 @@ homepage = "https://www.proxmox.com" [dependencies] anyhow.workspace = true -proxmox-auto-installer.workspace = true proxmox-installer-common = { workspace = true, features = ["http"] } proxmox-network-types.workspace = true +proxmox-installer-types.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true +toml.workspace = true diff --git a/proxmox-post-hook/src/main.rs b/proxmox-post-hook/src/main.rs index 8afe310..9086dc1 100644 --- a/proxmox-post-hook/src/main.rs +++ b/proxmox-post-hook/src/main.rs @@ -9,212 +9,49 @@ //! Relies on `proxmox-chroot` as an external dependency to (bind-)mount the //! previously installed system. -use anyhow::{Context, Result, anyhow, bail}; -use serde::Serialize; +use anyhow::{Context, Result, bail}; use std::{ - collections::HashSet, - ffi::CStr, - fs::{self, File}, - io::BufReader, - os::unix::fs::FileExt, - path::PathBuf, + fs, + io::Read, process::{Command, ExitCode}, }; -use proxmox_auto_installer::{ - answer::{ - Answer, FqdnConfig, FqdnExtendedConfig, FqdnSourceMode, PostNotificationHookInfo, - RebootMode, - }, - udevinfo::{UdevInfo, UdevProperties}, -}; -use proxmox_installer_common::{ - http::{self, header::HeaderMap}, - options::{Disk, FsType, NetworkOptions}, - setup::{ - BootType, InstallConfig, IsoInfo, ProxmoxProduct, RuntimeInfo, SetupInfo, - load_installer_setup_files, - }, - sysinfo::SystemDMI, -}; -use proxmox_network_types::ip_address::Cidr; +use proxmox_installer_common::http::{self, header::HeaderMap}; +use proxmox_installer_types::answer::{AutoInstallerConfig, PostNotificationHookInfo}; -/// Information about the system boot status. -#[derive(Serialize)] -struct BootInfo { - /// Whether the system is booted using UEFI or legacy BIOS. - mode: BootType, - /// Whether SecureBoot is enabled for the installation. - #[serde(skip_serializing_if = "bool_is_false")] - secureboot: bool, -} +/// Current version of the schema sent by this implementation. +const POST_HOOK_SCHEMA_VERSION: &str = "1.2"; -/// Holds all the public keys for the different algorithms available. -#[derive(Serialize)] -struct SshPublicHostKeys { - // ECDSA-based public host key - ecdsa: String, - // ED25519-based public host key - ed25519: String, - // RSA-based public host key - rsa: String, -} +mod detail { + use anyhow::{Context, Result, anyhow, bail}; + use std::{ + collections::HashSet, + ffi::CStr, + fs::{self, File}, + io::BufReader, + os::unix::fs::FileExt, + path::PathBuf, + process::Command, + }; -/// Holds information about a single disk in the system. -#[derive(Serialize)] -#[serde(rename_all = "kebab-case")] -struct DiskInfo { - /// Size in bytes - size: usize, - /// Set to true if the disk is used for booting. - #[serde(skip_serializing_if = "bool_is_false")] - is_bootdisk: bool, - /// Properties about the device as given by udev. - udev_properties: UdevProperties, -} + use proxmox_installer_common::{ + options::{Disk, NetworkOptions}, + setup::{ + InstallConfig, ProxmoxProduct, RuntimeInfo, SetupInfo, load_installer_setup_files, + }, + }; + use proxmox_installer_types::{ + BootType, IsoInfo, UdevInfo, + answer::{AutoInstallerConfig, FqdnConfig, FqdnFromDhcpConfig, FqdnSourceMode}, + post_hook::{ + BootInfo, CpuInfo, DiskInfo, KernelVersionInformation, NetworkInterfaceInfo, + PostHookInfo, PostHookInfoSchema, ProductInfo, SshPublicHostKeys, + }, + }; -/// Holds information about the management network interface. -#[derive(Serialize)] -#[serde(rename_all = "kebab-case")] -struct NetworkInterfaceInfo { - /// Name of the interface - name: String, - /// MAC address of the interface - mac: String, - /// (Designated) IP address of the interface - #[serde(skip_serializing_if = "Option::is_none")] - address: Option, - /// Set to true if the interface is the chosen management interface during - /// installation. - #[serde(skip_serializing_if = "bool_is_false")] - is_management: bool, - /// Set to true if the network interface name was pinned based on the MAC - /// address during the installation. - #[serde(skip_serializing_if = "bool_is_false")] - is_pinned: bool, - /// Properties about the device as given by udev. - udev_properties: UdevProperties, -} + /// Defines the size of a gibibyte in bytes. + const SIZE_GIB: usize = 1024 * 1024 * 1024; -fn bool_is_false(value: &bool) -> bool { - !value -} - -/// Information about the installed product itself. -#[derive(Serialize)] -#[serde(rename_all = "kebab-case")] -struct ProductInfo { - /// Full name of the product - fullname: String, - /// Product abbreviation - short: ProxmoxProduct, - /// Version of the installed product - version: String, -} - -/// The current kernel version. -/// Aligns with the format as used by the `/nodes//status` API of each product. -#[derive(Serialize)] -struct KernelVersionInformation { - /// The systemname/nodename - pub sysname: String, - /// The kernel release number - pub release: String, - /// The kernel version - pub version: String, - /// The machine architecture - pub machine: String, -} - -/// Information about the CPU(s) installed in the system -#[derive(Serialize)] -struct CpuInfo { - /// Number of physical CPU cores. - cores: usize, - /// Number of logical CPU cores aka. threads. - cpus: usize, - /// CPU feature flag set as a space-delimited list. - flags: String, - /// Whether hardware-accelerated virtualization is supported. - hvm: bool, - /// Reported model of the CPU(s) - model: String, - /// Number of physical CPU sockets - sockets: usize, -} - -/// Metadata of the hook, such as schema version of the document. -#[derive(Serialize)] -#[serde(rename_all = "kebab-case")] -struct PostHookInfoSchema { - /// major.minor version describing the schema version of this document, in a semanticy-version - /// way. - /// - /// major: Incremented for incompatible/breaking API changes, e.g. removing an existing - /// field. - /// minor: Incremented when adding functionality in a backwards-compatible matter, e.g. - /// adding a new field. - version: String, -} - -impl PostHookInfoSchema { - const SCHEMA_VERSION: &str = "1.2"; -} - -impl Default for PostHookInfoSchema { - fn default() -> Self { - Self { - version: Self::SCHEMA_VERSION.to_owned(), - } - } -} - -/// All data sent as request payload with the post-installation-webhook POST request. -/// -/// NOTE: The format is versioned through `schema.version` (`$schema.version` in the -/// resulting JSON), ensure you update it when this struct or any of its members gets modified. -#[derive(Serialize)] -#[serde(rename_all = "kebab-case")] -struct PostHookInfo { - // This field is prefixed by `$` on purpose, to indicate that it is document metadata and not - // part of the actual content itself. (E.g. JSON Schema uses a similar naming scheme) - #[serde(rename = "$schema")] - schema: PostHookInfoSchema, - /// major.minor version of Debian as installed, retrieved from /etc/debian_version - debian_version: String, - /// PVE/PMG/PBS/PDM version as reported by `pveversion`, `pmgversion`, - /// `proxmox-backup-manager version` or `proxmox-datacenter-manager version`, respectively. - product: ProductInfo, - /// Release information for the ISO used for the installation. - iso: IsoInfo, - /// Installed kernel version - kernel_version: KernelVersionInformation, - /// Describes the boot mode of the machine and the SecureBoot status. - boot_info: BootInfo, - /// Information about the installed CPU(s) - cpu_info: CpuInfo, - /// DMI information about the system - dmi: SystemDMI, - /// Filesystem used for boot disk(s) - filesystem: FsType, - /// Fully qualified domain name of the installed system - fqdn: String, - /// Unique systemd-id128 identifier of the installed system (128-bit, 16 bytes) - machine_id: String, - /// All disks detected on the system. - disks: Vec, - /// All network interfaces detected on the system. - network_interfaces: Vec, - /// Public parts of SSH host keys of the installed system - ssh_public_host_keys: SshPublicHostKeys, - /// Action to will be performed, i.e. either reboot or power off the machine. - reboot_mode: RebootMode, -} - -/// Defines the size of a gibibyte in bytes. -const SIZE_GIB: usize = 1024 * 1024 * 1024; - -impl PostHookInfo { /// Gathers all needed information about the newly installed system for sending /// it to a specified server. /// @@ -222,7 +59,7 @@ impl PostHookInfo { /// /// * `target_path` - Path to where the chroot environment root is mounted /// * `answer` - Answer file as provided by the user - fn gather(target_path: &str, answer: &Answer) -> Result { + pub fn gather(target_path: &str, answer: &AutoInstallerConfig) -> Result { println!("Gathering installed system data ..."); let config: InstallConfig = @@ -265,34 +102,42 @@ impl PostHookInfo { let fqdn = match &answer.global.fqdn { FqdnConfig::Simple(name) => name.to_string(), - FqdnConfig::Extended(FqdnExtendedConfig { + FqdnConfig::FromDhcp(FqdnFromDhcpConfig { source: FqdnSourceMode::FromDhcp, domain, }) => NetworkOptions::construct_fqdn( &run_env.network, - setup_info.config.product.default_hostname(), + &setup_info.config.product.to_string(), domain.as_deref(), ) .to_string(), }; - Ok(Self { - schema: PostHookInfoSchema::default(), + Ok(PostHookInfo { + schema: PostHookInfoSchema { + version: super::POST_HOOK_SCHEMA_VERSION.to_owned(), + }, debian_version: read_file("/etc/debian_version")?, - product: Self::gather_product_info(&setup_info, &run_cmd)?, - iso: setup_info.iso_info.clone(), - kernel_version: Self::gather_kernel_version(&run_cmd, &open_file)?, + product: gather_product_info(&setup_info, &run_cmd)?, + iso: IsoInfo { + release: setup_info.iso_info.release, + isorelease: setup_info.iso_info.isorelease, + }, + kernel_version: gather_kernel_version(&run_cmd, &open_file)?, boot_info: BootInfo { - mode: run_env.boot_type, + mode: match run_env.boot_type { + proxmox_installer_common::setup::BootType::Bios => BootType::Bios, + proxmox_installer_common::setup::BootType::Efi => BootType::Efi, + }, secureboot: run_env.secure_boot, }, - cpu_info: Self::gather_cpu_info(&run_env)?, - dmi: SystemDMI::get()?, - filesystem: answer.disks.fs_type, + cpu_info: gather_cpu_info(&run_env)?, + dmi: proxmox_installer_common::dmi::get()?, + filesystem: answer.disks.filesystem_details()?.to_type(), fqdn, machine_id: read_file("/etc/machine-id")?, - disks: Self::gather_disks(&config, &run_env, &udev)?, - network_interfaces: Self::gather_nic(&config, &run_env, &udev)?, + disks: gather_disks(&config, &run_env, &udev)?, + network_interfaces: gather_nic(&config, &run_env, &udev)?, ssh_public_host_keys: SshPublicHostKeys { ecdsa: read_file("/etc/ssh/ssh_host_ecdsa_key.pub")?, ed25519: read_file("/etc/ssh/ssh_host_ed25519_key.pub")?, @@ -335,10 +180,10 @@ impl PostHookInfo { .target_hd .as_ref() .map(|hd| *hd == disk.path) - .unwrap_or_default(); + .unwrap_or(false); anyhow::Ok(DiskInfo { - size: (config.hdsize * (SIZE_GIB as f64)) as usize, + size: (config.hdsize * (SIZE_GIB as f64)) as u64, is_bootdisk, udev_properties: get_udev_properties(disk)?, }) @@ -346,7 +191,7 @@ impl PostHookInfo { .collect() } else { // If the filesystem is not LVM-based (thus Btrfs or ZFS), `config.disk_selection` - // contains a list of indices identifiying the boot disks, as given by udev. + // contains a list of indices identifying the boot disks, as given by udev. let selected_disks_indices: Vec<&String> = config.disk_selection.values().collect(); run_env @@ -356,7 +201,7 @@ impl PostHookInfo { let is_bootdisk = selected_disks_indices.contains(&&disk.index); anyhow::Ok(DiskInfo { - size: (config.hdsize * (SIZE_GIB as f64)) as usize, + size: (config.hdsize * (SIZE_GIB as f64)) as u64, is_bootdisk, udev_properties: get_udev_properties(disk)?, }) @@ -443,7 +288,12 @@ impl PostHookInfo { Ok(ProductInfo { fullname: setup_info.config.fullname.clone(), - short: setup_info.config.product, + short: match setup_info.config.product { + ProxmoxProduct::PVE => proxmox_installer_types::ProxmoxProduct::Pve, + ProxmoxProduct::PBS => proxmox_installer_types::ProxmoxProduct::Pbs, + ProxmoxProduct::PMG => proxmox_installer_types::ProxmoxProduct::Pmg, + ProxmoxProduct::PDM => proxmox_installer_types::ProxmoxProduct::Pdm, + }, version, }) } @@ -465,7 +315,7 @@ impl PostHookInfo { run_cmd: &dyn Fn(&[&str]) -> Result, open_file: &dyn Fn(&str) -> Result, ) -> Result { - let file = open_file(&Self::find_kernel_image_path(run_cmd)?)?; + let file = open_file(&find_kernel_image_path(run_cmd)?)?; // Read the 2-byte `kernel_version` field at offset 0x20e [0] from the file .. // https://www.kernel.org/doc/html/latest/arch/x86/boot.html#the-real-mode-kernel-header @@ -525,7 +375,7 @@ impl PostHookInfo { run_cmd: &dyn Fn(&[&str]) -> Result, _open_file: &dyn Fn(&str) -> Result, ) -> Result { - let image_path = Self::find_kernel_image_path(run_cmd)?; + let image_path = find_kernel_image_path(run_cmd)?; let release = image_path .strip_prefix("/boot/vmlinuz-") @@ -537,11 +387,7 @@ impl PostHookInfo { .map(|v| { // /proc/version: "Linux version 6.17.2-1-pve (...) #1 SMP ..." // extract everything after the second space - v.splitn(3, ' ') - .nth(2) - .unwrap_or("") - .trim() - .to_owned() + v.splitn(3, ' ').nth(2).unwrap_or("").trim().to_owned() }) .unwrap_or_default(); @@ -560,7 +406,7 @@ impl PostHookInfo { /// /// * `run_cmd` - Callback to run a command inside the target chroot. fn find_kernel_image_path(run_cmd: &dyn Fn(&[&str]) -> Result) -> Result { - let pkg_name = Self::find_kernel_package_name(run_cmd)?; + let pkg_name = find_kernel_package_name(run_cmd)?; let all_files = run_cmd(&["dpkg-query", "--listfiles", &pkg_name])?; for file in all_files.lines() { @@ -667,6 +513,200 @@ impl PostHookInfo { Ok(result) } + + #[cfg(test)] + mod tests { + use super::{find_kernel_image_path, find_kernel_package_name}; + + #[test] + fn finds_correct_kernel_package_name() { + let mocked_run_cmd = |cmd: &[&str]| { + if cmd[0] == "dpkg" { + assert_eq!(cmd, &["dpkg", "--print-architecture"]); + Ok("amd64\n".to_owned()) + } else { + assert_eq!( + cmd, + &[ + "dpkg-query", + "--showformat", + "${db:Status-Abbrev}|${Architecture}|${Package}\\n", + "--show", + "proxmox-kernel-[0-9]*", + ] + ); + Ok(r#"ii |all|proxmox-kernel-6.8 +un ||proxmox-kernel-6.8.8-2-pve +ii |amd64|proxmox-kernel-6.8.8-2-pve-signed + "# + .to_owned()) + } + }; + + assert_eq!( + find_kernel_package_name(&mocked_run_cmd).unwrap(), + "proxmox-kernel-6.8.8-2-pve-signed" + ); + } + + #[test] + fn finds_correct_kernel_package_name_arm64() { + let mocked_run_cmd = |cmd: &[&str]| { + if cmd[0] == "dpkg" { + assert_eq!(cmd, &["dpkg", "--print-architecture"]); + Ok("arm64\n".to_owned()) + } else { + assert_eq!( + cmd, + &[ + "dpkg-query", + "--showformat", + "${db:Status-Abbrev}|${Architecture}|${Package}\\n", + "--show", + "proxmox-kernel-[0-9]*", + ] + ); + Ok(r#"ii |all|proxmox-kernel-6.17 +un ||proxmox-kernel-6.17.2-1-pve +ii |arm64|proxmox-kernel-6.17.2-1-pve-signed + "# + .to_owned()) + } + }; + + assert_eq!( + find_kernel_package_name(&mocked_run_cmd).unwrap(), + "proxmox-kernel-6.17.2-1-pve-signed" + ); + } + + #[test] + fn find_kernel_package_name_fails_on_wrong_architecture() { + let mocked_run_cmd = |cmd: &[&str]| { + if cmd[0] == "dpkg" { + assert_eq!(cmd, &["dpkg", "--print-architecture"]); + Ok("arm64\n".to_owned()) + } else { + assert_eq!( + cmd, + &[ + "dpkg-query", + "--showformat", + "${db:Status-Abbrev}|${Architecture}|${Package}\\n", + "--show", + "proxmox-kernel-[0-9]*", + ] + ); + Ok(r#"ii |all|proxmox-kernel-6.8 +un ||proxmox-kernel-6.8.8-2-pve +ii |amd64|proxmox-kernel-6.8.8-2-pve-signed + "# + .to_owned()) + } + }; + + assert_eq!( + find_kernel_package_name(&mocked_run_cmd) + .unwrap_err() + .to_string(), + "failed to find installed kernel package" + ); + } + + #[test] + fn find_kernel_package_name_fails_on_missing_package() { + let mocked_run_cmd = |cmd: &[&str]| { + if cmd[0] == "dpkg" { + assert_eq!(cmd, &["dpkg", "--print-architecture"]); + Ok("amd64\n".to_owned()) + } else { + assert_eq!( + cmd, + &[ + "dpkg-query", + "--showformat", + "${db:Status-Abbrev}|${Architecture}|${Package}\\n", + "--show", + "proxmox-kernel-[0-9]*", + ] + ); + Ok(r#"ii |all|proxmox-kernel-6.8 +un ||proxmox-kernel-6.8.8-2-pve + "# + .to_owned()) + } + }; + + assert_eq!( + find_kernel_package_name(&mocked_run_cmd) + .unwrap_err() + .to_string(), + "failed to find installed kernel package" + ); + } + + #[test] + fn finds_correct_absolute_kernel_image_path() { + let mocked_run_cmd = |cmd: &[&str]| { + if cmd[0] == "dpkg" { + assert_eq!(cmd, &["dpkg", "--print-architecture"]); + Ok("amd64\n".to_owned()) + } else if cmd[0..=1] == ["dpkg-query", "--showformat"] { + assert_eq!( + cmd, + &[ + "dpkg-query", + "--showformat", + "${db:Status-Abbrev}|${Architecture}|${Package}\\n", + "--show", + "proxmox-kernel-[0-9]*", + ] + ); + Ok(r#"ii |all|proxmox-kernel-6.8 +un ||proxmox-kernel-6.8.8-2-pve +ii |amd64|proxmox-kernel-6.8.8-2-pve-signed + "# + .to_owned()) + } else { + assert_eq!( + cmd, + [ + "dpkg-query", + "--listfiles", + "proxmox-kernel-6.8.8-2-pve-signed" + ] + ); + Ok(r#" +/. +/boot +/boot/System.map-6.8.8-2-pve +/boot/config-6.8.8-2-pve +/boot/vmlinuz-6.8.8-2-pve +/lib +/lib/modules +/lib/modules/6.8.8-2-pve +/lib/modules/6.8.8-2-pve/kernel +/lib/modules/6.8.8-2-pve/kernel/arch +/lib/modules/6.8.8-2-pve/kernel/arch/x86 +/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto +/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/aegis128-aesni.ko +/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/aesni-intel.ko +/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/aria-aesni-avx-x86_64.ko +/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/aria-aesni-avx2-x86_64.ko +/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/aria-gfni-avx512-x86_64.ko +/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/blowfish-x86_64.ko +/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/camellia-aesni-avx-x86_64.ko + "# + .to_owned()) + } + }; + + assert_eq!( + find_kernel_image_path(&mocked_run_cmd).unwrap(), + "/boot/vmlinuz-6.8.8-2-pve" + ); + } + } } /// Runs the specified callback with the mounted chroot, passing along the @@ -704,7 +744,9 @@ fn with_chroot Result>(callback: F) -> Result { /// optional certificate fingerprint for HTTPS). If configured, retrieves all relevant information /// about the installed system and sends them to the given endpoint. fn do_main() -> Result<()> { - let answer = Answer::try_from_reader(std::io::stdin().lock())?; + let mut raw_toml = String::new(); + std::io::stdin().read_to_string(&mut raw_toml)?; + let answer: AutoInstallerConfig = toml::from_str(&raw_toml)?; if let Some(PostNotificationHookInfo { url, @@ -713,7 +755,7 @@ fn do_main() -> Result<()> { { println!("Found post-installation-webhook; sending POST request to '{url}'."); - let info = with_chroot(|target_path| PostHookInfo::gather(target_path, &answer))?; + let info = with_chroot(|target_path| detail::gather(target_path, &answer))?; if let Err(err) = fs::write( "/run/proxmox-installer/post-hook-data.json", @@ -747,197 +789,3 @@ fn main() -> ExitCode { } } } - -#[cfg(test)] -mod tests { - use crate::PostHookInfo; - - #[test] - fn finds_correct_kernel_package_name() { - let mocked_run_cmd = |cmd: &[&str]| { - if cmd[0] == "dpkg" { - assert_eq!(cmd, &["dpkg", "--print-architecture"]); - Ok("amd64\n".to_owned()) - } else { - assert_eq!( - cmd, - &[ - "dpkg-query", - "--showformat", - "${db:Status-Abbrev}|${Architecture}|${Package}\\n", - "--show", - "proxmox-kernel-[0-9]*", - ] - ); - Ok(r#"ii |all|proxmox-kernel-6.8 -un ||proxmox-kernel-6.8.8-2-pve -ii |amd64|proxmox-kernel-6.8.8-2-pve-signed - "# - .to_owned()) - } - }; - - assert_eq!( - PostHookInfo::find_kernel_package_name(&mocked_run_cmd).unwrap(), - "proxmox-kernel-6.8.8-2-pve-signed" - ); - } - - #[test] - fn finds_correct_kernel_package_name_arm64() { - let mocked_run_cmd = |cmd: &[&str]| { - if cmd[0] == "dpkg" { - assert_eq!(cmd, &["dpkg", "--print-architecture"]); - Ok("arm64\n".to_owned()) - } else { - assert_eq!( - cmd, - &[ - "dpkg-query", - "--showformat", - "${db:Status-Abbrev}|${Architecture}|${Package}\\n", - "--show", - "proxmox-kernel-[0-9]*", - ] - ); - Ok(r#"ii |all|proxmox-kernel-6.17 -un ||proxmox-kernel-6.17.2-1-pve -ii |arm64|proxmox-kernel-6.17.2-1-pve-signed - "# - .to_owned()) - } - }; - - assert_eq!( - PostHookInfo::find_kernel_package_name(&mocked_run_cmd).unwrap(), - "proxmox-kernel-6.17.2-1-pve-signed" - ); - } - - #[test] - fn find_kernel_package_name_fails_on_wrong_architecture() { - let mocked_run_cmd = |cmd: &[&str]| { - if cmd[0] == "dpkg" { - assert_eq!(cmd, &["dpkg", "--print-architecture"]); - Ok("arm64\n".to_owned()) - } else { - assert_eq!( - cmd, - &[ - "dpkg-query", - "--showformat", - "${db:Status-Abbrev}|${Architecture}|${Package}\\n", - "--show", - "proxmox-kernel-[0-9]*", - ] - ); - Ok(r#"ii |all|proxmox-kernel-6.8 -un ||proxmox-kernel-6.8.8-2-pve -ii |amd64|proxmox-kernel-6.8.8-2-pve-signed - "# - .to_owned()) - } - }; - - assert_eq!( - PostHookInfo::find_kernel_package_name(&mocked_run_cmd) - .unwrap_err() - .to_string(), - "failed to find installed kernel package" - ); - } - - #[test] - fn find_kernel_package_name_fails_on_missing_package() { - let mocked_run_cmd = |cmd: &[&str]| { - if cmd[0] == "dpkg" { - assert_eq!(cmd, &["dpkg", "--print-architecture"]); - Ok("amd64\n".to_owned()) - } else { - assert_eq!( - cmd, - &[ - "dpkg-query", - "--showformat", - "${db:Status-Abbrev}|${Architecture}|${Package}\\n", - "--show", - "proxmox-kernel-[0-9]*", - ] - ); - Ok(r#"ii |all|proxmox-kernel-6.8 -un ||proxmox-kernel-6.8.8-2-pve - "# - .to_owned()) - } - }; - - assert_eq!( - PostHookInfo::find_kernel_package_name(&mocked_run_cmd) - .unwrap_err() - .to_string(), - "failed to find installed kernel package" - ); - } - - #[test] - fn finds_correct_absolute_kernel_image_path() { - let mocked_run_cmd = |cmd: &[&str]| { - if cmd[0] == "dpkg" { - assert_eq!(cmd, &["dpkg", "--print-architecture"]); - Ok("amd64\n".to_owned()) - } else if cmd[0..=1] == ["dpkg-query", "--showformat"] { - assert_eq!( - cmd, - &[ - "dpkg-query", - "--showformat", - "${db:Status-Abbrev}|${Architecture}|${Package}\\n", - "--show", - "proxmox-kernel-[0-9]*", - ] - ); - Ok(r#"ii |all|proxmox-kernel-6.8 -un ||proxmox-kernel-6.8.8-2-pve -ii |amd64|proxmox-kernel-6.8.8-2-pve-signed - "# - .to_owned()) - } else { - assert_eq!( - cmd, - [ - "dpkg-query", - "--listfiles", - "proxmox-kernel-6.8.8-2-pve-signed" - ] - ); - Ok(r#" -/. -/boot -/boot/System.map-6.8.8-2-pve -/boot/config-6.8.8-2-pve -/boot/vmlinuz-6.8.8-2-pve -/lib -/lib/modules -/lib/modules/6.8.8-2-pve -/lib/modules/6.8.8-2-pve/kernel -/lib/modules/6.8.8-2-pve/kernel/arch -/lib/modules/6.8.8-2-pve/kernel/arch/x86 -/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto -/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/aegis128-aesni.ko -/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/aesni-intel.ko -/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/aria-aesni-avx-x86_64.ko -/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/aria-aesni-avx2-x86_64.ko -/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/aria-gfni-avx512-x86_64.ko -/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/blowfish-x86_64.ko -/lib/modules/6.8.8-2-pve/kernel/arch/x86/crypto/camellia-aesni-avx-x86_64.ko - "# - .to_owned()) - } - }; - - assert_eq!( - PostHookInfo::find_kernel_image_path(&mocked_run_cmd).unwrap(), - "/boot/vmlinuz-6.8.8-2-pve" - ); - } -} diff --git a/proxmox-tui-installer/Cargo.toml b/proxmox-tui-installer/Cargo.toml index 1ca91cb..56395a4 100644 --- a/proxmox-tui-installer/Cargo.toml +++ b/proxmox-tui-installer/Cargo.toml @@ -10,6 +10,7 @@ homepage = "https://www.proxmox.com" [dependencies] proxmox-installer-common.workspace = true proxmox-network-types.workspace = true +proxmox-installer-types.workspace = true anyhow.workspace = true serde_json.workspace = true diff --git a/proxmox-tui-installer/src/options.rs b/proxmox-tui-installer/src/options.rs index c80877f..ff15fa0 100644 --- a/proxmox-tui-installer/src/options.rs +++ b/proxmox-tui-installer/src/options.rs @@ -2,29 +2,10 @@ use crate::SummaryOption; use proxmox_installer_common::{ EMAIL_DEFAULT_PLACEHOLDER, - options::{ - BootdiskOptions, BtrfsRaidLevel, FsType, NetworkOptions, TimezoneOptions, ZfsRaidLevel, - }, + options::{BootdiskOptions, NetworkOptions, TimezoneOptions}, setup::LocaleInfo, }; -pub const FS_TYPES: &[FsType] = { - use FsType::*; - &[ - Ext4, - Xfs, - Zfs(ZfsRaidLevel::Raid0), - Zfs(ZfsRaidLevel::Raid1), - Zfs(ZfsRaidLevel::Raid10), - Zfs(ZfsRaidLevel::RaidZ), - Zfs(ZfsRaidLevel::RaidZ2), - Zfs(ZfsRaidLevel::RaidZ3), - Btrfs(BtrfsRaidLevel::Raid0), - Btrfs(BtrfsRaidLevel::Raid1), - Btrfs(BtrfsRaidLevel::Raid10), - ] -}; - #[derive(Clone)] pub struct PasswordOptions { pub email: String, diff --git a/proxmox-tui-installer/src/views/bootdisk.rs b/proxmox-tui-installer/src/views/bootdisk.rs index 5ec3e83..ed3936f 100644 --- a/proxmox-tui-installer/src/views/bootdisk.rs +++ b/proxmox-tui-installer/src/views/bootdisk.rs @@ -16,7 +16,6 @@ use cursive::{ use super::{DiskSizeEditView, FormView, IntegerEditView, TabbedView}; use crate::InstallerState; -use crate::options::FS_TYPES; use proxmox_installer_common::{ disk_checks::{ @@ -24,11 +23,12 @@ use proxmox_installer_common::{ }, options::{ AdvancedBootdiskOptions, BTRFS_COMPRESS_OPTIONS, BootdiskOptions, BtrfsBootdiskOptions, - Disk, FsType, LvmBootdiskOptions, ZFS_CHECKSUM_OPTIONS, ZFS_COMPRESS_OPTIONS, + Disk, LvmBootdiskOptions, RaidLevel, ZFS_CHECKSUM_OPTIONS, ZFS_COMPRESS_OPTIONS, ZfsBootdiskOptions, }, setup::{BootType, ProductConfig, ProxmoxProduct, RuntimeInfo}, }; +use proxmox_installer_types::answer::{FILESYSTEM_TYPE_OPTIONS, FilesystemType}; /// OpenZFS specifies 64 MiB as the absolute minimum: /// @@ -125,19 +125,19 @@ impl AdvancedBootdiskOptionsView { product_conf: ProductConfig, ) -> Self { let filter_btrfs = - |fstype: &&FsType| -> bool { product_conf.enable_btrfs || !fstype.is_btrfs() }; + |fstype: &&FilesystemType| -> bool { product_conf.enable_btrfs || !fstype.is_btrfs() }; let options = options_ref.lock().unwrap(); let fstype_select = SelectView::new() .popup() .with_all( - FS_TYPES + FILESYSTEM_TYPE_OPTIONS .iter() .filter(filter_btrfs) .map(|t| (t.to_string(), *t)), ) .selected( - FS_TYPES + FILESYSTEM_TYPE_OPTIONS .iter() .filter(filter_btrfs) .position(|t| *t == options.fstype) @@ -185,7 +185,11 @@ impl AdvancedBootdiskOptionsView { /// * `fstype` - The chosen filesystem type by the user, for which the UI should be /// updated accordingly /// * `options_ref` - [`BootdiskOptionsRef`] where advanced disk options should be saved to - fn fstype_on_submit(siv: &mut Cursive, fstype: &FsType, options_ref: BootdiskOptionsRef) { + fn fstype_on_submit( + siv: &mut Cursive, + fstype: &FilesystemType, + options_ref: BootdiskOptionsRef, + ) { let state = siv.user_data::().unwrap(); let runinfo = state.runtime_info.clone(); let product_conf = state.setup_info.config.clone(); @@ -208,16 +212,16 @@ impl AdvancedBootdiskOptionsView { { view.remove_child(3); match fstype { - FsType::Ext4 | FsType::Xfs => { + FilesystemType::Ext4 | FilesystemType::Xfs => { view.add_child(LvmBootdiskOptionsView::new_with_defaults( &selected_lvm_disk, &product_conf, )) } - FsType::Zfs(_) => { + FilesystemType::Zfs(_) => { view.add_child(ZfsBootdiskOptionsView::new_with_defaults(&runinfo)) } - FsType::Btrfs(_) => { + FilesystemType::Btrfs(_) => { view.add_child(BtrfsBootdiskOptionsView::new_with_defaults(&runinfo)) } } @@ -236,7 +240,7 @@ impl AdvancedBootdiskOptionsView { siv.call_on_name( "bootdisk-options-target-disk", move |view: &mut FormView| match fstype { - FsType::Ext4 | FsType::Xfs => { + FilesystemType::Ext4 | FilesystemType::Xfs => { view.replace_child( 0, target_bootdisk_selectview(&runinfo.disks, options_ref, &selected_lvm_disk), @@ -252,7 +256,7 @@ impl AdvancedBootdiskOptionsView { .view .get_child(1) .and_then(|v| v.downcast_ref::()) - .and_then(|v| v.get_value::, _>(0)) + .and_then(|v| v.get_value::, _>(0)) .ok_or("Failed to retrieve filesystem type".to_owned())?; let advanced = self @@ -279,7 +283,7 @@ impl AdvancedBootdiskOptionsView { .get_values() .ok_or("Failed to retrieve advanced bootdisk options")?; - if let FsType::Zfs(level) = fstype { + if let FilesystemType::Zfs(level) = fstype { level .check_raid_disks_setup(&disks) .map_err(|err| format!("{fstype}: {err}"))?; @@ -295,7 +299,7 @@ impl AdvancedBootdiskOptionsView { .get_values() .ok_or("Failed to retrieve advanced bootdisk options")?; - if let FsType::Btrfs(level) = fstype { + if let FilesystemType::Btrfs(level) = fstype { level .check_raid_disks_setup(&disks) .map_err(|err| format!("{fstype}: {err}"))?; -- 2.53.0