-use std::{cell::RefCell, collections::HashSet, marker::PhantomData, rc::Rc};
+use std::{cell::RefCell, marker::PhantomData, rc::Rc};
use cursive::{
view::{Nameable, Resizable, ViewWrapper},
};
use super::{DiskSizeEditView, FormView, IntegerEditView};
-use crate::options::{
- AdvancedBootdiskOptions, BootdiskOptions, BtrfsBootdiskOptions, Disk, FsType,
- LvmBootdiskOptions, ZfsBootdiskOptions, FS_TYPES, ZFS_CHECKSUM_OPTIONS, ZFS_COMPRESS_OPTIONS,
+use crate::options::FS_TYPES;
+use crate::InstallerState;
+
+use proxmox_installer_common::{
+ disk_checks::{
+ check_btrfs_raid_config, check_disks_4kn_legacy_boot, check_for_duplicate_disks,
+ check_zfs_raid_config,
+ },
+ options::{
+ AdvancedBootdiskOptions, BootdiskOptions, BtrfsBootdiskOptions, Disk, FsType,
+ LvmBootdiskOptions, ZfsBootdiskOptions, ZFS_CHECKSUM_OPTIONS, ZFS_COMPRESS_OPTIONS,
+ },
+ setup::{BootType, ProductConfig, ProxmoxProduct, RuntimeInfo},
};
-use crate::setup::ProxmoxProduct;
+
+/// OpenZFS specifies 64 MiB as the absolute minimum:
+/// https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Module%20Parameters.html#zfs-arc-max
+const ZFS_ARC_MIN_SIZE_MIB: usize = 64; // MiB
+
+/// Convience wrapper when needing to take a (interior-mutable) reference to `BootdiskOptions`.
+/// Interior mutability is safe for this case, as it is completely single-threaded.
+pub type BootdiskOptionsRef = Rc<RefCell<BootdiskOptions>>;
pub struct BootdiskOptionsView {
view: LinearLayout,
- advanced_options: Rc<RefCell<BootdiskOptions>>,
+ advanced_options: BootdiskOptionsRef,
+ boot_type: BootType,
}
impl BootdiskOptionsView {
- pub fn new(disks: &[Disk], options: &BootdiskOptions) -> Self {
+ pub fn new(siv: &mut Cursive, runinfo: &RuntimeInfo, options: &BootdiskOptions) -> Self {
let bootdisk_form = FormView::new()
.child(
"Target harddisk",
SelectView::new()
.popup()
- .with_all(disks.iter().map(|d| (d.to_string(), d.clone()))),
+ .with_all(runinfo.disks.iter().map(|d| (d.to_string(), d.clone()))),
)
.with_name("bootdisk-options-target-disk");
+ let product_conf = siv
+ .user_data::<InstallerState>()
+ .map(|state| state.setup_info.config.clone())
+ .unwrap(); // Safety: InstallerState must always be set
+
let advanced_options = Rc::new(RefCell::new(options.clone()));
let advanced_button = LinearLayout::horizontal()
.child(DummyView.full_width())
.child(Button::new("Advanced options", {
- let disks = disks.to_owned();
+ let runinfo = runinfo.clone();
let options = advanced_options.clone();
move |siv| {
- siv.add_layer(advanced_options_view(&disks, options.clone()));
+ siv.add_layer(advanced_options_view(
+ &runinfo,
+ options.clone(),
+ product_conf.clone(),
+ ));
}
}));
.child(DummyView)
.child(advanced_button);
+ let boot_type = siv
+ .user_data::<InstallerState>()
+ .map(|state| state.runtime_info.boot_type)
+ .unwrap_or(BootType::Bios);
+
Self {
view,
advanced_options,
+ boot_type,
}
}
options.disks = vec![disk];
}
+ check_disks_4kn_legacy_boot(self.boot_type, &options.disks)?;
Ok(options)
}
}
}
impl AdvancedBootdiskOptionsView {
- fn new(disks: &[Disk], options: &BootdiskOptions) -> Self {
- let enable_btrfs = crate::setup_info().config.enable_btrfs;
-
- let filter_btrfs = |fstype: &&FsType| -> bool { enable_btrfs || !fstype.is_btrfs() };
+ fn new(runinfo: &RuntimeInfo, options: &BootdiskOptions, product_conf: ProductConfig) -> Self {
+ let filter_btrfs =
+ |fstype: &&FsType| -> bool { product_conf.enable_btrfs || !fstype.is_btrfs() };
let fstype_select = SelectView::new()
.popup()
.position(|t| *t == options.fstype)
.unwrap_or_default(),
)
- .on_submit({
- let disks = disks.to_owned();
- move |siv, fstype| Self::fstype_on_submit(siv, &disks, fstype)
- });
+ .on_submit(Self::fstype_on_submit);
let mut view = LinearLayout::vertical()
.child(DummyView.full_width())
.child(DummyView.full_width());
match &options.advanced {
- AdvancedBootdiskOptions::Lvm(lvm) => view.add_child(LvmBootdiskOptionsView::new(lvm)),
+ AdvancedBootdiskOptions::Lvm(lvm) => {
+ view.add_child(LvmBootdiskOptionsView::new(lvm, &product_conf))
+ }
AdvancedBootdiskOptions::Zfs(zfs) => {
- view.add_child(ZfsBootdiskOptionsView::new(disks, zfs))
+ view.add_child(ZfsBootdiskOptionsView::new(runinfo, zfs, &product_conf))
}
AdvancedBootdiskOptions::Btrfs(btrfs) => {
- view.add_child(BtrfsBootdiskOptionsView::new(disks, btrfs))
+ view.add_child(BtrfsBootdiskOptionsView::new(&runinfo.disks, btrfs))
}
};
Self { view }
}
- fn fstype_on_submit(siv: &mut Cursive, disks: &[Disk], fstype: &FsType) {
+ fn fstype_on_submit(siv: &mut Cursive, fstype: &FsType) {
+ let state = siv.user_data::<InstallerState>().unwrap();
+ let runinfo = state.runtime_info.clone();
+ let product_conf = state.setup_info.config.clone();
+
siv.call_on_name("advanced-bootdisk-options-dialog", |view: &mut Dialog| {
if let Some(AdvancedBootdiskOptionsView { view }) =
view.get_content_mut().downcast_mut()
{
view.remove_child(3);
match fstype {
- FsType::Ext4 | FsType::Xfs => view.add_child(LvmBootdiskOptionsView::new(
- &LvmBootdiskOptions::defaults_from(&disks[0]),
- )),
- FsType::Zfs(_) => view.add_child(ZfsBootdiskOptionsView::new(
- disks,
- &ZfsBootdiskOptions::defaults_from(disks),
- )),
- FsType::Btrfs(_) => view.add_child(BtrfsBootdiskOptionsView::new(
- disks,
- &BtrfsBootdiskOptions::defaults_from(disks),
+ FsType::Ext4 | FsType::Xfs => view.add_child(
+ LvmBootdiskOptionsView::new_with_defaults(&runinfo.disks[0], &product_conf),
+ ),
+ FsType::Zfs(_) => view.add_child(ZfsBootdiskOptionsView::new_with_defaults(
+ &runinfo,
+ &product_conf,
)),
+ FsType::Btrfs(_) => {
+ view.add_child(BtrfsBootdiskOptionsView::new_with_defaults(&runinfo.disks))
+ }
}
}
});
0,
SelectView::new()
.popup()
- .with_all(disks.iter().map(|d| (d.to_string(), d.clone()))),
+ .with_all(runinfo.disks.iter().map(|d| (d.to_string(), d.clone()))),
);
}
other => view.replace_child(0, TextView::new(other.to_string())),
);
}
- fn get_values(&mut self) -> Option<BootdiskOptions> {
+ fn get_values(&mut self) -> Result<BootdiskOptions, String> {
let fstype = self
.view
- .get_child(1)?
- .downcast_ref::<FormView>()?
- .get_value::<SelectView<FsType>, _>(0)?;
+ .get_child(1)
+ .and_then(|v| v.downcast_ref::<FormView>())
+ .and_then(|v| v.get_value::<SelectView<FsType>, _>(0))
+ .ok_or("Failed to retrieve filesystem type".to_owned())?;
- let advanced = self.view.get_child_mut(3)?;
+ let advanced = self
+ .view
+ .get_child_mut(3)
+ .ok_or("Failed to retrieve advanced bootdisk options view".to_owned())?;
if let Some(view) = advanced.downcast_mut::<LvmBootdiskOptionsView>() {
- Some(BootdiskOptions {
+ let advanced = view
+ .get_values()
+ .map(AdvancedBootdiskOptions::Lvm)
+ .ok_or("Failed to retrieve advanced bootdisk options")?;
+
+ Ok(BootdiskOptions {
disks: vec![],
fstype,
- advanced: view.get_values().map(AdvancedBootdiskOptions::Lvm)?,
+ advanced,
})
} else if let Some(view) = advanced.downcast_mut::<ZfsBootdiskOptionsView>() {
- let (disks, advanced) = view.get_values()?;
+ let (disks, advanced) = view
+ .get_values()
+ .ok_or("Failed to retrieve advanced bootdisk options")?;
+
+ if let FsType::Zfs(level) = fstype {
+ check_zfs_raid_config(level, &disks).map_err(|err| format!("{fstype}: {err}"))?;
+ }
- Some(BootdiskOptions {
+ Ok(BootdiskOptions {
disks,
fstype,
advanced: AdvancedBootdiskOptions::Zfs(advanced),
})
} else if let Some(view) = advanced.downcast_mut::<BtrfsBootdiskOptionsView>() {
- let (disks, advanced) = view.get_values()?;
+ let (disks, advanced) = view
+ .get_values()
+ .ok_or("Failed to retrieve advanced bootdisk options")?;
- Some(BootdiskOptions {
+ if let FsType::Btrfs(level) = fstype {
+ check_btrfs_raid_config(level, &disks).map_err(|err| format!("{fstype}: {err}"))?;
+ }
+
+ Ok(BootdiskOptions {
disks,
fstype,
advanced: AdvancedBootdiskOptions::Btrfs(advanced),
})
} else {
- None
+ Err("Invalid bootdisk view state".to_owned())
}
}
}
struct LvmBootdiskOptionsView {
view: FormView,
+ has_extra_fields: bool,
}
impl LvmBootdiskOptionsView {
- fn new(options: &LvmBootdiskOptions) -> Self {
- let is_pve = crate::setup_info().config.product == ProxmoxProduct::PVE;
+ fn new(options: &LvmBootdiskOptions, product_conf: &ProductConfig) -> Self {
+ let show_extra_fields = product_conf.product == ProxmoxProduct::PVE;
+
// TODO: Set maximum accordingly to disk size
let view = FormView::new()
.child(
DiskSizeEditView::new_emptyable().content_maybe(options.swap_size),
)
.child_conditional(
- is_pve,
+ show_extra_fields,
"Maximum root volume size",
DiskSizeEditView::new_emptyable().content_maybe(options.max_root_size),
)
.child_conditional(
- is_pve,
+ show_extra_fields,
"Maximum data volume size",
DiskSizeEditView::new_emptyable().content_maybe(options.max_data_size),
)
DiskSizeEditView::new_emptyable().content_maybe(options.min_lvm_free),
);
- Self { view }
+ Self {
+ view,
+ has_extra_fields: show_extra_fields,
+ }
+ }
+
+ fn new_with_defaults(disk: &Disk, product_conf: &ProductConfig) -> Self {
+ Self::new(&LvmBootdiskOptions::defaults_from(disk), product_conf)
}
fn get_values(&mut self) -> Option<LvmBootdiskOptions> {
- let is_pve = crate::setup_info().config.product == ProxmoxProduct::PVE;
- let min_lvm_free_id = if is_pve { 4 } else { 2 };
- let max_root_size = if is_pve {
- self.view.get_value::<DiskSizeEditView, _>(2)
- } else {
- None
- };
- let max_data_size = if is_pve {
- self.view.get_value::<DiskSizeEditView, _>(3)
- } else {
- None
- };
+ let min_lvm_free_id = if self.has_extra_fields { 4 } else { 2 };
+
+ let max_root_size = self
+ .has_extra_fields
+ .then(|| self.view.get_value::<DiskSizeEditView, _>(2))
+ .flatten();
+ let max_data_size = self
+ .has_extra_fields
+ .then(|| self.view.get_value::<DiskSizeEditView, _>(3))
+ .flatten();
+
Some(LvmBootdiskOptions {
total_size: self.view.get_value::<DiskSizeEditView, _>(0)?,
swap_size: self.view.get_value::<DiskSizeEditView, _>(1),
Self { view }
}
+ fn new_with_defaults(disks: &[Disk]) -> Self {
+ Self::new(disks, &BtrfsBootdiskOptions::defaults_from(disks))
+ }
+
fn get_values(&mut self) -> Option<(Vec<Disk>, BtrfsBootdiskOptions)> {
let (disks, selected_disks) = self.view.get_disks_and_selection()?;
let disk_size = self.view.inner_mut()?.get_value::<DiskSizeEditView, _>(0)?;
impl ZfsBootdiskOptionsView {
// TODO: Re-apply previous disk selection from `options` correctly
- fn new(disks: &[Disk], options: &ZfsBootdiskOptions) -> Self {
+ fn new(
+ runinfo: &RuntimeInfo,
+ options: &ZfsBootdiskOptions,
+ product_conf: &ProductConfig,
+ ) -> Self {
+ let is_pve = product_conf.product == ProxmoxProduct::PVE;
+
let inner = FormView::new()
.child("ashift", IntegerEditView::new().content(options.ashift))
.child(
),
)
.child("copies", IntegerEditView::new().content(options.copies))
+ .child_conditional(
+ is_pve,
+ "ARC max size",
+ IntegerEditView::new_with_suffix("MiB")
+ .max_value(runinfo.total_memory)
+ .content(options.arc_max),
+ )
.child("hdsize", DiskSizeEditView::new().content(options.disk_size));
- let view = MultiDiskOptionsView::new(disks, &options.selected_disks, inner)
+ let view = MultiDiskOptionsView::new(&runinfo.disks, &options.selected_disks, inner)
.top_panel(TextView::new(
"ZFS is not compatible with hardware RAID controllers, for details see the documentation."
).center());
Self { view }
}
+ fn new_with_defaults(runinfo: &RuntimeInfo, product_conf: &ProductConfig) -> Self {
+ Self::new(
+ runinfo,
+ &ZfsBootdiskOptions::defaults_from(runinfo, product_conf),
+ product_conf,
+ )
+ }
+
fn get_values(&mut self) -> Option<(Vec<Disk>, ZfsBootdiskOptions)> {
let (disks, selected_disks) = self.view.get_disks_and_selection()?;
let view = self.view.inner_mut()?;
+ let has_arc_max = view.len() >= 6;
+ let disk_size_index = if has_arc_max { 5 } else { 4 };
let ashift = view.get_value::<IntegerEditView, _>(0)?;
let compress = view.get_value::<SelectView<_>, _>(1)?;
let checksum = view.get_value::<SelectView<_>, _>(2)?;
let copies = view.get_value::<IntegerEditView, _>(3)?;
- let disk_size = view.get_value::<DiskSizeEditView, _>(4)?;
+ let disk_size = view.get_value::<DiskSizeEditView, _>(disk_size_index)?;
+
+ let arc_max = if has_arc_max {
+ view.get_value::<IntegerEditView, _>(4)?
+ .max(ZFS_ARC_MIN_SIZE_MIB)
+ } else {
+ 0 // use built-in ZFS default value
+ };
Some((
disks,
compress,
checksum,
copies,
+ arc_max,
disk_size,
selected_disks,
},
cursive::wrap_impl!(self.view: MultiDiskOptionsView<FormView>);
}
-fn advanced_options_view(disks: &[Disk], options: Rc<RefCell<BootdiskOptions>>) -> impl View {
+fn advanced_options_view(
+ runinfo: &RuntimeInfo,
+ options_ref: BootdiskOptionsRef,
+ product_conf: ProductConfig,
+) -> impl View {
Dialog::around(AdvancedBootdiskOptionsView::new(
- disks,
- &(*options).borrow(),
+ runinfo,
+ &(*options_ref).borrow(),
+ product_conf,
))
.title("Advanced bootdisk options")
.button("Ok", {
- let options_ref = options.clone();
+ let options_ref = options_ref.clone();
move |siv| {
let options = siv
.call_on_name("advanced-bootdisk-options-dialog", |view: &mut Dialog| {
view.get_content_mut()
- .downcast_mut()
- .and_then(AdvancedBootdiskOptionsView::get_values)
+ .downcast_mut::<AdvancedBootdiskOptionsView>()
+ .map(AdvancedBootdiskOptionsView::get_values)
})
.flatten();
+ let options = match options {
+ Some(Ok(options)) => options,
+ Some(Err(err)) => {
+ siv.add_layer(Dialog::info(err));
+ return;
+ }
+ None => {
+ siv.add_layer(Dialog::info("Failed to retrieve bootdisk options view"));
+ return;
+ }
+ };
+
if let Err(duplicate) = check_for_duplicate_disks(&options.disks) {
siv.add_layer(Dialog::info(format!(
"Cannot select same disk twice: {duplicate}"
}
siv.pop_layer();
- if let Some(options) = options {
- *(*options_ref).borrow_mut() = options;
- }
+ *(*options_ref).borrow_mut() = options;
}
})
.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(())
-}