1 use std
::{cell::RefCell, marker::PhantomData, rc::Rc}
;
4 view
::{Nameable, Resizable, ViewWrapper}
,
6 Button
, Dialog
, DummyView
, LinearLayout
, NamedView
, PaddedView
, Panel
, ScrollView
,
12 use super::{DiskSizeEditView, FormView, IntegerEditView}
;
13 use crate::options
::FS_TYPES
;
14 use crate::InstallerState
;
16 use proxmox_installer_common
::{
18 check_btrfs_raid_config
, check_disks_4kn_legacy_boot
, check_for_duplicate_disks
,
19 check_zfs_raid_config
,
22 AdvancedBootdiskOptions
, BootdiskOptions
, BtrfsBootdiskOptions
, Disk
, FsType
,
23 LvmBootdiskOptions
, ZfsBootdiskOptions
, ZFS_CHECKSUM_OPTIONS
, ZFS_COMPRESS_OPTIONS
,
25 setup
::{BootType, ProductConfig, ProxmoxProduct, RuntimeInfo}
,
28 /// OpenZFS specifies 64 MiB as the absolute minimum:
29 /// https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Module%20Parameters.html#zfs-arc-max
30 const ZFS_ARC_MIN_SIZE_MIB
: usize = 64; // MiB
32 /// Convience wrapper when needing to take a (interior-mutable) reference to `BootdiskOptions`.
33 /// Interior mutability is safe for this case, as it is completely single-threaded.
34 pub type BootdiskOptionsRef
= Rc
<RefCell
<BootdiskOptions
>>;
36 pub struct BootdiskOptionsView
{
38 advanced_options
: BootdiskOptionsRef
,
42 impl BootdiskOptionsView
{
43 pub fn new(siv
: &mut Cursive
, runinfo
: &RuntimeInfo
, options
: &BootdiskOptions
) -> Self {
44 let advanced_options
= Rc
::new(RefCell
::new(options
.clone()));
46 let bootdisk_form
= FormView
::new()
49 target_bootdisk_selectview(
51 advanced_options
.clone(),
52 // At least one disk must always exist to even get to this point,
53 // see proxmox_installer_common::setup::installer_setup()
57 .with_name("bootdisk-options-target-disk");
59 let product_conf
= siv
60 .user_data
::<InstallerState
>()
61 .map(|state
| state
.setup_info
.config
.clone())
62 .unwrap(); // Safety: InstallerState must always be set
64 let advanced_button
= LinearLayout
::horizontal()
65 .child(DummyView
.full_width())
66 .child(Button
::new("Advanced options", {
67 let runinfo
= runinfo
.clone();
68 let options
= advanced_options
.clone();
70 siv
.add_layer(advanced_options_view(
78 let view
= LinearLayout
::vertical()
81 .child(advanced_button
);
84 .user_data
::<InstallerState
>()
85 .map(|state
| state
.runtime_info
.boot_type
)
86 .unwrap_or(BootType
::Bios
);
95 pub fn get_values(&mut self) -> Result
<BootdiskOptions
, String
> {
96 // The simple disk selector, as well as the advanced bootdisk dialog save their
97 // info on submit directly to the shared `BootdiskOptionsRef` - so just clone() + return
99 let options
= (*self.advanced_options
).clone().into_inner();
100 check_disks_4kn_legacy_boot(self.boot_type
, &options
.disks
)?
;
105 impl ViewWrapper
for BootdiskOptionsView
{
106 cursive
::wrap_impl
!(self.view
: LinearLayout
);
109 struct AdvancedBootdiskOptionsView
{
113 impl AdvancedBootdiskOptionsView
{
115 runinfo
: &RuntimeInfo
,
116 options_ref
: BootdiskOptionsRef
,
117 product_conf
: ProductConfig
,
120 |fstype
: &&FsType
| -> bool { product_conf.enable_btrfs || !fstype.is_btrfs() }
;
121 let options
= (*options_ref
).borrow();
123 let fstype_select
= SelectView
::new()
128 .filter(filter_btrfs
)
129 .map(|t
| (t
.to_string(), *t
)),
134 .filter(filter_btrfs
)
135 .position(|t
| *t
== options
.fstype
)
136 .unwrap_or_default(),
139 let options_ref
= options_ref
.clone();
141 Self::fstype_on_submit(siv
, fstype
, options_ref
.clone());
145 let mut view
= LinearLayout
::vertical()
146 .child(DummyView
.full_width())
147 .child(FormView
::new().child("Filesystem", fstype_select
))
148 .child(DummyView
.full_width());
150 // Create the appropriate (inner) advanced options view
151 match &options
.advanced
{
152 AdvancedBootdiskOptions
::Lvm(lvm
) => view
.add_child(LvmBootdiskOptionsView
::new(
157 AdvancedBootdiskOptions
::Zfs(zfs
) => {
158 view
.add_child(ZfsBootdiskOptionsView
::new(runinfo
, zfs
, &product_conf
))
160 AdvancedBootdiskOptions
::Btrfs(btrfs
) => {
161 view
.add_child(BtrfsBootdiskOptionsView
::new(&runinfo
.disks
, btrfs
))
168 /// Called when a new filesystem type is choosen by the user.
169 /// It first creates the inner (filesystem-specific) options view according to the selected
171 /// Further, it replaces the (outer) bootdisk selector in the main dialog, either with a
172 /// selector for LVM configurations or a simple label displaying the chosen RAID for ZFS and
173 /// Btrfs configurations.
176 /// * `siv` - Cursive instance
177 /// * `fstype` - The chosen filesystem type by the user, for which the UI should be
178 /// updated accordingly
179 /// * `options_ref` - [`BootdiskOptionsRef`] where advanced disk options should be saved to
180 fn fstype_on_submit(siv
: &mut Cursive
, fstype
: &FsType
, options_ref
: BootdiskOptionsRef
) {
181 let state
= siv
.user_data
::<InstallerState
>().unwrap();
182 let runinfo
= state
.runtime_info
.clone();
183 let product_conf
= state
.setup_info
.config
.clone();
185 // Only used for LVM configurations, ZFS and Btrfs do not use the target disk selector
186 // Must be done here, as we cannot mutable borrow `siv` a second time inside the closure
188 let selected_lvm_disk
= siv
189 .find_name
::<FormView
>("bootdisk-options-target-disk")
190 .and_then(|v
| v
.get_value
::<SelectView
<Disk
>, _
>(0))
191 // If not defined, then the view was switched from a non-LVM filesystem to a LVM one.
192 // Just use the first disk is such a case.
193 .unwrap_or_else(|| runinfo
.disks
[0].clone());
195 // Update the (inner) options view
196 siv
.call_on_name("advanced-bootdisk-options-dialog", |view
: &mut Dialog
| {
197 if let Some(AdvancedBootdiskOptionsView { view }
) =
198 view
.get_content_mut().downcast_mut()
200 view
.remove_child(3);
202 FsType
::Ext4
| FsType
::Xfs
=> {
203 view
.add_child(LvmBootdiskOptionsView
::new_with_defaults(
208 FsType
::Zfs(_
) => view
.add_child(ZfsBootdiskOptionsView
::new_with_defaults(
212 FsType
::Btrfs(_
) => {
213 view
.add_child(BtrfsBootdiskOptionsView
::new_with_defaults(&runinfo
.disks
))
219 // The "bootdisk-options-target-disk" view might be either a `SelectView` (if ext4 of XFS
220 // is used) or a label containing the filesytem/RAID type (for ZFS and Btrfs).
221 // Now, unconditionally replace it with the appropriate type of these two, depending on the
222 // newly selected filesystem type.
224 "bootdisk-options-target-disk",
225 move |view
: &mut FormView
| match fstype
{
226 FsType
::Ext4
| FsType
::Xfs
=> {
229 target_bootdisk_selectview(&runinfo
.disks
, options_ref
, &selected_lvm_disk
),
232 other
=> view
.replace_child(0, TextView
::new(other
.to_string())),
237 fn get_values(&mut self) -> Result
<BootdiskOptions
, String
> {
241 .and_then(|v
| v
.downcast_ref
::<FormView
>())
242 .and_then(|v
| v
.get_value
::<SelectView
<FsType
>, _
>(0))
243 .ok_or("Failed to retrieve filesystem type".to_owned())?
;
248 .ok_or("Failed to retrieve advanced bootdisk options view".to_owned())?
;
250 if let Some(view
) = advanced
.downcast_mut
::<LvmBootdiskOptionsView
>() {
251 let (disk
, advanced
) = view
253 .ok_or("Failed to retrieve advanced bootdisk options")?
;
258 advanced
: AdvancedBootdiskOptions
::Lvm(advanced
),
260 } else if let Some(view
) = advanced
.downcast_mut
::<ZfsBootdiskOptionsView
>() {
261 let (disks
, advanced
) = view
263 .ok_or("Failed to retrieve advanced bootdisk options")?
;
265 if let FsType
::Zfs(level
) = fstype
{
266 check_zfs_raid_config(level
, &disks
).map_err(|err
| format
!("{fstype}: {err}"))?
;
272 advanced
: AdvancedBootdiskOptions
::Zfs(advanced
),
274 } else if let Some(view
) = advanced
.downcast_mut
::<BtrfsBootdiskOptionsView
>() {
275 let (disks
, advanced
) = view
277 .ok_or("Failed to retrieve advanced bootdisk options")?
;
279 if let FsType
::Btrfs(level
) = fstype
{
280 check_btrfs_raid_config(level
, &disks
).map_err(|err
| format
!("{fstype}: {err}"))?
;
286 advanced
: AdvancedBootdiskOptions
::Btrfs(advanced
),
289 Err("Invalid bootdisk view state".to_owned())
294 impl ViewWrapper
for AdvancedBootdiskOptionsView
{
295 cursive
::wrap_impl
!(self.view
: LinearLayout
);
298 struct LvmBootdiskOptionsView
{
301 has_extra_fields
: bool
,
304 impl LvmBootdiskOptionsView
{
305 fn new(disk
: &Disk
, options
: &LvmBootdiskOptions
, product_conf
: &ProductConfig
) -> Self {
306 let show_extra_fields
= product_conf
.product
== ProxmoxProduct
::PVE
;
308 let view
= FormView
::new()
311 DiskSizeEditView
::new()
312 .content(options
.total_size
)
313 .max_value(options
.total_size
),
317 DiskSizeEditView
::new_emptyable().content_maybe(options
.swap_size
),
321 "Maximum root volume size",
322 DiskSizeEditView
::new_emptyable().content_maybe(options
.max_root_size
),
326 "Maximum data volume size",
327 DiskSizeEditView
::new_emptyable().content_maybe(options
.max_data_size
),
330 "Minimum free LVM space",
331 DiskSizeEditView
::new_emptyable().content_maybe(options
.min_lvm_free
),
337 has_extra_fields
: show_extra_fields
,
341 fn new_with_defaults(disk
: &Disk
, product_conf
: &ProductConfig
) -> Self {
342 Self::new(disk
, &LvmBootdiskOptions
::defaults_from(disk
), product_conf
)
345 fn get_values(&mut self) -> Option
<(Disk
, LvmBootdiskOptions
)> {
346 let min_lvm_free_id
= if self.has_extra_fields { 4 }
else { 2 }
;
348 let max_root_size
= self
350 .then(|| self.view
.get_value
::<DiskSizeEditView
, _
>(2))
352 let max_data_size
= self
354 .then(|| self.view
.get_value
::<DiskSizeEditView
, _
>(3))
360 total_size
: self.view
.get_value
::<DiskSizeEditView
, _
>(0)?
,
361 swap_size
: self.view
.get_value
::<DiskSizeEditView
, _
>(1),
364 min_lvm_free
: self.view
.get_value
::<DiskSizeEditView
, _
>(min_lvm_free_id
),
370 impl ViewWrapper
for LvmBootdiskOptionsView
{
371 cursive
::wrap_impl
!(self.view
: FormView
);
374 struct MultiDiskOptionsView
<T
> {
376 phantom
: PhantomData
<T
>,
379 impl<T
: View
> MultiDiskOptionsView
<T
> {
380 fn new(avail_disks
: &[Disk
], selected_disks
: &[usize], options_view
: T
) -> Self {
381 let mut selectable_disks
= avail_disks
383 .map(|d
| (d
.to_string(), Some(d
.clone())))
384 .collect
::<Vec
<(String
, Option
<Disk
>)>>();
386 selectable_disks
.push(("-- do not use --".to_owned(), None
));
388 let mut disk_form
= FormView
::new();
389 for (i
, _
) in avail_disks
.iter().enumerate() {
391 &format
!("Harddisk {i}"),
394 .with_all(selectable_disks
.clone())
395 .selected(selected_disks
[i
]),
399 let mut disk_select_view
= LinearLayout
::vertical()
400 .child(TextView
::new("Disk setup").center())
402 .child(ScrollView
::new(disk_form
.with_name("multidisk-disk-form")));
404 if avail_disks
.len() > 3 {
405 let do_not_use_index
= selectable_disks
.len() - 1;
406 let deselect_all_button
= Button
::new("Deselect all", move |siv
| {
407 siv
.call_on_name("multidisk-disk-form", |view
: &mut FormView
| {
408 view
.call_on_childs(&|v
: &mut SelectView
<Option
<Disk
>>| {
409 // As there is no .on_select() callback defined on the
410 // SelectView's, the returned callback here can be safely
412 v
.set_selection(do_not_use_index
);
417 disk_select_view
.add_child(PaddedView
::lrtb(
422 LinearLayout
::horizontal()
423 .child(DummyView
.full_width())
424 .child(deselect_all_button
),
428 let options_view
= LinearLayout
::vertical()
429 .child(TextView
::new("Advanced options").center())
431 .child(options_view
);
433 let view
= LinearLayout
::horizontal()
434 .child(disk_select_view
)
435 .child(DummyView
.fixed_width(3))
436 .child(options_view
);
439 view
: LinearLayout
::vertical().child(view
),
440 phantom
: PhantomData
,
444 fn top_panel(mut self, view
: impl View
) -> Self {
445 if self.has_top_panel() {
446 self.view
.remove_child(0);
449 self.view
.insert_child(0, Panel
::new(view
));
454 /// This function returns a tuple of vectors. The first vector contains the currently selected
455 /// disks in order of their selection slot. Empty slots are filtered out. The second vector
456 /// contains indices of each slot's selection, which enables us to restore the selection even
459 fn get_disks_and_selection(&mut self) -> Option
<(Vec
<Disk
>, Vec
<usize>)> {
460 let mut disks
= vec
![];
461 let view_top_index
= usize::from(self.has_top_panel());
465 .get_child_mut(view_top_index
)?
466 .downcast_mut
::<LinearLayout
>()?
468 .downcast_mut
::<LinearLayout
>()?
470 .downcast_mut
::<ScrollView
<NamedView
<FormView
>>>()?
474 let mut selected_disks
= Vec
::new();
476 for i
in 0..disk_form
.len() {
477 let disk
= disk_form
.get_value
::<SelectView
<Option
<Disk
>>, _
>(i
)?
;
479 // `None` means no disk was selected for this slot
480 if let Some(disk
) = disk
{
486 .get_child
::<SelectView
<Option
<Disk
>>>(i
)?
491 Some((disks
, selected_disks
))
494 fn inner_mut(&mut self) -> Option
<&mut T
> {
495 let view_top_index
= usize::from(self.has_top_panel());
498 .get_child_mut(view_top_index
)?
499 .downcast_mut
::<LinearLayout
>()?
501 .downcast_mut
::<LinearLayout
>()?
506 fn has_top_panel(&self) -> bool
{
507 // The root view should only ever have one or two children
508 assert
!([1, 2].contains(&self.view
.len()));
514 impl<T
: '
static> ViewWrapper
for MultiDiskOptionsView
<T
> {
515 cursive
::wrap_impl
!(self.view
: LinearLayout
);
518 struct BtrfsBootdiskOptionsView
{
519 view
: MultiDiskOptionsView
<FormView
>,
522 impl BtrfsBootdiskOptionsView
{
523 fn new(disks
: &[Disk
], options
: &BtrfsBootdiskOptions
) -> Self {
524 let view
= MultiDiskOptionsView
::new(
526 &options
.selected_disks
,
527 FormView
::new().child("hdsize", DiskSizeEditView
::new().content(options
.disk_size
)),
529 .top_panel(TextView
::new("Btrfs integration is a technology preview!").center());
534 fn new_with_defaults(disks
: &[Disk
]) -> Self {
535 Self::new(disks
, &BtrfsBootdiskOptions
::defaults_from(disks
))
538 fn get_values(&mut self) -> Option
<(Vec
<Disk
>, BtrfsBootdiskOptions
)> {
539 let (disks
, selected_disks
) = self.view
.get_disks_and_selection()?
;
540 let disk_size
= self.view
.inner_mut()?
.get_value
::<DiskSizeEditView
, _
>(0)?
;
544 BtrfsBootdiskOptions
{
552 impl ViewWrapper
for BtrfsBootdiskOptionsView
{
553 cursive
::wrap_impl
!(self.view
: MultiDiskOptionsView
<FormView
>);
556 struct ZfsBootdiskOptionsView
{
557 view
: MultiDiskOptionsView
<FormView
>,
560 impl ZfsBootdiskOptionsView
{
561 // TODO: Re-apply previous disk selection from `options` correctly
563 runinfo
: &RuntimeInfo
,
564 options
: &ZfsBootdiskOptions
,
565 product_conf
: &ProductConfig
,
567 let is_pve
= product_conf
.product
== ProxmoxProduct
::PVE
;
569 let inner
= FormView
::new()
570 .child("ashift", IntegerEditView
::new().content(options
.ashift
))
575 .with_all(ZFS_COMPRESS_OPTIONS
.iter().map(|o
| (o
.to_string(), *o
)))
579 .position(|o
| *o
== options
.compress
)
580 .unwrap_or_default(),
587 .with_all(ZFS_CHECKSUM_OPTIONS
.iter().map(|o
| (o
.to_string(), *o
)))
591 .position(|o
| *o
== options
.checksum
)
592 .unwrap_or_default(),
595 .child("copies", IntegerEditView
::new().content(options
.copies
).max_value(3))
599 IntegerEditView
::new_with_suffix("MiB")
600 .max_value(runinfo
.total_memory
)
601 .content(options
.arc_max
),
603 .child("hdsize", DiskSizeEditView
::new().content(options
.disk_size
));
605 let view
= MultiDiskOptionsView
::new(&runinfo
.disks
, &options
.selected_disks
, inner
)
606 .top_panel(TextView
::new(
607 "ZFS is not compatible with hardware RAID controllers, for details see the documentation."
613 fn new_with_defaults(runinfo
: &RuntimeInfo
, product_conf
: &ProductConfig
) -> Self {
616 &ZfsBootdiskOptions
::defaults_from(runinfo
, product_conf
),
621 fn get_values(&mut self) -> Option
<(Vec
<Disk
>, ZfsBootdiskOptions
)> {
622 let (disks
, selected_disks
) = self.view
.get_disks_and_selection()?
;
623 let view
= self.view
.inner_mut()?
;
624 let has_arc_max
= view
.len() >= 6;
625 let disk_size_index
= if has_arc_max { 5 }
else { 4 }
;
627 let ashift
= view
.get_value
::<IntegerEditView
, _
>(0)?
;
628 let compress
= view
.get_value
::<SelectView
<_
>, _
>(1)?
;
629 let checksum
= view
.get_value
::<SelectView
<_
>, _
>(2)?
;
630 let copies
= view
.get_value
::<IntegerEditView
, _
>(3)?
;
631 let disk_size
= view
.get_value
::<DiskSizeEditView
, _
>(disk_size_index
)?
;
633 let arc_max
= if has_arc_max
{
634 view
.get_value
::<IntegerEditView
, _
>(4)?
635 .max(ZFS_ARC_MIN_SIZE_MIB
)
637 0 // use built-in ZFS default value
655 impl ViewWrapper
for ZfsBootdiskOptionsView
{
656 cursive
::wrap_impl
!(self.view
: MultiDiskOptionsView
<FormView
>);
659 fn advanced_options_view(
660 runinfo
: &RuntimeInfo
,
661 options_ref
: BootdiskOptionsRef
,
662 product_conf
: ProductConfig
,
664 Dialog
::around(AdvancedBootdiskOptionsView
::new(
669 .title("Advanced bootdisk options")
673 .call_on_name("advanced-bootdisk-options-dialog", |view
: &mut Dialog
| {
674 view
.get_content_mut()
675 .downcast_mut
::<AdvancedBootdiskOptionsView
>()
676 .map(AdvancedBootdiskOptionsView
::get_values
)
680 let options
= match options
{
681 Some(Ok(options
)) => options
,
683 siv
.add_layer(Dialog
::info(err
));
687 siv
.add_layer(Dialog
::info("Failed to retrieve bootdisk options view"));
692 if let Err(duplicate
) = check_for_duplicate_disks(&options
.disks
) {
693 siv
.add_layer(Dialog
::info(format
!(
694 "Cannot select same disk twice: {duplicate}"
700 *(*options_ref
).borrow_mut() = options
;
703 .with_name("advanced-bootdisk-options-dialog")
707 /// Creates a select view for all disks specified.
711 /// * `avail_disks` - Disks that should be shown in the select view
712 /// * `options_ref` - [`BootdiskOptionsRef`] where advanced disk options should be saved to
713 /// * `selected_disk` - Optional, specifies which disk should be pre-selected
714 fn target_bootdisk_selectview(
715 avail_disks
: &[Disk
],
716 options_ref
: BootdiskOptionsRef
,
717 selected_disk
: &Disk
,
718 ) -> SelectView
<Disk
> {
719 let selected_disk_pos
= avail_disks
721 .position(|d
| d
.index
== selected_disk
.index
)
722 .unwrap_or_default();
726 .with_all(avail_disks
.iter().map(|d
| (d
.to_string(), d
.clone())))
727 .selected(selected_disk_pos
)
728 .on_submit(move |_
, disk
| {
729 options_ref
.borrow_mut().disks
= vec
![disk
.clone()];
730 options_ref
.borrow_mut().advanced
=
731 AdvancedBootdiskOptions
::Lvm(LvmBootdiskOptions
::defaults_from(disk
));