tui: switch to common crate

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

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
Reviewed-by: Christoph Heiss <c.heiss@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
Aaron Lauterer 2023-10-25 18:00:06 +02:00 committed by Thomas Lamprecht
parent 41f2a23371
commit 86c48f76f0
7 changed files with 44 additions and 944 deletions

View File

@ -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,

View File

@ -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@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 {

View File

@ -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)
}
}

View File

@ -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")

View File

@ -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());
}
}

View File

@ -7,7 +7,7 @@ use cursive::{
Rect, Vec2, View,
};
use crate::utils::CidrAddress;
use proxmox_installer_common::utils::CidrAddress;
mod bootdisk;
pub use bootdisk::*;

View File

@ -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 {