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 pub struct BootdiskOptionsView
{
30 advanced_options
: Rc
<RefCell
<BootdiskOptions
>>,
34 impl BootdiskOptionsView
{
35 pub fn new(siv
: &mut Cursive
, disks
: &[Disk
], options
: &BootdiskOptions
) -> Self {
36 let bootdisk_form
= FormView
::new()
41 .with_all(disks
.iter().map(|d
| (d
.to_string(), d
.clone()))),
43 .with_name("bootdisk-options-target-disk");
45 let product_conf
= siv
46 .user_data
::<InstallerState
>()
47 .map(|state
| state
.setup_info
.config
.clone())
48 .unwrap(); // Safety: InstallerState must always be set
50 let advanced_options
= Rc
::new(RefCell
::new(options
.clone()));
52 let advanced_button
= LinearLayout
::horizontal()
53 .child(DummyView
.full_width())
54 .child(Button
::new("Advanced options", {
55 let disks
= disks
.to_owned();
56 let options
= advanced_options
.clone();
58 siv
.add_layer(advanced_options_view(
66 let view
= LinearLayout
::vertical()
69 .child(advanced_button
);
72 .user_data
::<InstallerState
>()
73 .map(|state
| state
.runtime_info
.boot_type
)
74 .unwrap_or(BootType
::Bios
);
83 pub fn get_values(&mut self) -> Result
<BootdiskOptions
, String
> {
84 let mut options
= (*self.advanced_options
).clone().into_inner();
86 if [FsType
::Ext4
, FsType
::Xfs
].contains(&options
.fstype
) {
90 .and_then(|v
| v
.downcast_mut
::<NamedView
<FormView
>>())
91 .map(NamedView
::<FormView
>::get_mut
)
92 .and_then(|v
| v
.get_value
::<SelectView
<Disk
>, _
>(0))
93 .ok_or("failed to retrieve bootdisk")?
;
95 options
.disks
= vec
![disk
];
98 check_disks_4kn_legacy_boot(self.boot_type
, &options
.disks
)?
;
103 impl ViewWrapper
for BootdiskOptionsView
{
104 cursive
::wrap_impl
!(self.view
: LinearLayout
);
107 struct AdvancedBootdiskOptionsView
{
111 impl AdvancedBootdiskOptionsView
{
112 fn new(disks
: &[Disk
], options
: &BootdiskOptions
, product_conf
: ProductConfig
) -> Self {
114 |fstype
: &&FsType
| -> bool { product_conf.enable_btrfs || !fstype.is_btrfs() }
;
116 let fstype_select
= SelectView
::new()
121 .filter(filter_btrfs
)
122 .map(|t
| (t
.to_string(), *t
)),
127 .filter(filter_btrfs
)
128 .position(|t
| *t
== options
.fstype
)
129 .unwrap_or_default(),
132 let disks
= disks
.to_owned();
133 move |siv
, fstype
| Self::fstype_on_submit(siv
, &disks
, fstype
)
136 let mut view
= LinearLayout
::vertical()
137 .child(DummyView
.full_width())
138 .child(FormView
::new().child("Filesystem", fstype_select
))
139 .child(DummyView
.full_width());
141 match &options
.advanced
{
142 AdvancedBootdiskOptions
::Lvm(lvm
) => {
143 view
.add_child(LvmBootdiskOptionsView
::new(lvm
, &product_conf
))
145 AdvancedBootdiskOptions
::Zfs(zfs
) => {
146 view
.add_child(ZfsBootdiskOptionsView
::new(disks
, zfs
))
148 AdvancedBootdiskOptions
::Btrfs(btrfs
) => {
149 view
.add_child(BtrfsBootdiskOptionsView
::new(disks
, btrfs
))
156 fn fstype_on_submit(siv
: &mut Cursive
, disks
: &[Disk
], fstype
: &FsType
) {
157 let state
= siv
.user_data
::<InstallerState
>().unwrap();
158 let runinfo
= state
.runtime_info
.clone();
159 let product_conf
= state
.setup_info
.config
.clone();
161 siv
.call_on_name("advanced-bootdisk-options-dialog", |view
: &mut Dialog
| {
162 if let Some(AdvancedBootdiskOptionsView { view }
) =
163 view
.get_content_mut().downcast_mut()
165 view
.remove_child(3);
167 FsType
::Ext4
| FsType
::Xfs
=> view
.add_child(
168 LvmBootdiskOptionsView
::new_with_defaults(&disks
[0], &product_conf
),
170 FsType
::Zfs(_
) => view
.add_child(ZfsBootdiskOptionsView
::new_with_defaults(
174 FsType
::Btrfs(_
) => {
175 view
.add_child(BtrfsBootdiskOptionsView
::new_with_defaults(disks
))
182 "bootdisk-options-target-disk",
183 |view
: &mut FormView
| match fstype
{
184 FsType
::Ext4
| FsType
::Xfs
=> {
189 .with_all(disks
.iter().map(|d
| (d
.to_string(), d
.clone()))),
192 other
=> view
.replace_child(0, TextView
::new(other
.to_string())),
197 fn get_values(&mut self) -> Result
<BootdiskOptions
, String
> {
201 .and_then(|v
| v
.downcast_ref
::<FormView
>())
202 .and_then(|v
| v
.get_value
::<SelectView
<FsType
>, _
>(0))
203 .ok_or("Failed to retrieve filesystem type".to_owned())?
;
208 .ok_or("Failed to retrieve advanced bootdisk options view".to_owned())?
;
210 if let Some(view
) = advanced
.downcast_mut
::<LvmBootdiskOptionsView
>() {
213 .map(AdvancedBootdiskOptions
::Lvm
)
214 .ok_or("Failed to retrieve advanced bootdisk options")?
;
221 } else if let Some(view
) = advanced
.downcast_mut
::<ZfsBootdiskOptionsView
>() {
222 let (disks
, advanced
) = view
224 .ok_or("Failed to retrieve advanced bootdisk options")?
;
226 if let FsType
::Zfs(level
) = fstype
{
227 check_zfs_raid_config(level
, &disks
).map_err(|err
| format
!("{fstype}: {err}"))?
;
233 advanced
: AdvancedBootdiskOptions
::Zfs(advanced
),
235 } else if let Some(view
) = advanced
.downcast_mut
::<BtrfsBootdiskOptionsView
>() {
236 let (disks
, advanced
) = view
238 .ok_or("Failed to retrieve advanced bootdisk options")?
;
240 if let FsType
::Btrfs(level
) = fstype
{
241 check_btrfs_raid_config(level
, &disks
).map_err(|err
| format
!("{fstype}: {err}"))?
;
247 advanced
: AdvancedBootdiskOptions
::Btrfs(advanced
),
250 Err("Invalid bootdisk view state".to_owned())
255 impl ViewWrapper
for AdvancedBootdiskOptionsView
{
256 cursive
::wrap_impl
!(self.view
: LinearLayout
);
259 struct LvmBootdiskOptionsView
{
261 has_extra_fields
: bool
,
264 impl LvmBootdiskOptionsView
{
265 fn new(options
: &LvmBootdiskOptions
, product_conf
: &ProductConfig
) -> Self {
266 let show_extra_fields
= product_conf
.product
== ProxmoxProduct
::PVE
;
268 // TODO: Set maximum accordingly to disk size
269 let view
= FormView
::new()
272 DiskSizeEditView
::new()
273 .content(options
.total_size
)
274 .max_value(options
.total_size
),
278 DiskSizeEditView
::new_emptyable().content_maybe(options
.swap_size
),
282 "Maximum root volume size",
283 DiskSizeEditView
::new_emptyable().content_maybe(options
.max_root_size
),
287 "Maximum data volume size",
288 DiskSizeEditView
::new_emptyable().content_maybe(options
.max_data_size
),
291 "Minimum free LVM space",
292 DiskSizeEditView
::new_emptyable().content_maybe(options
.min_lvm_free
),
297 has_extra_fields
: show_extra_fields
,
301 fn new_with_defaults(disk
: &Disk
, product_conf
: &ProductConfig
) -> Self {
302 Self::new(&LvmBootdiskOptions
::defaults_from(disk
), product_conf
)
305 fn get_values(&mut self) -> Option
<LvmBootdiskOptions
> {
306 let min_lvm_free_id
= if self.has_extra_fields { 4 }
else { 2 }
;
308 let max_root_size
= self
310 .then(|| self.view
.get_value
::<DiskSizeEditView
, _
>(2))
312 let max_data_size
= self
314 .then(|| self.view
.get_value
::<DiskSizeEditView
, _
>(3))
317 Some(LvmBootdiskOptions
{
318 total_size
: self.view
.get_value
::<DiskSizeEditView
, _
>(0)?
,
319 swap_size
: self.view
.get_value
::<DiskSizeEditView
, _
>(1),
322 min_lvm_free
: self.view
.get_value
::<DiskSizeEditView
, _
>(min_lvm_free_id
),
327 impl ViewWrapper
for LvmBootdiskOptionsView
{
328 cursive
::wrap_impl
!(self.view
: FormView
);
331 struct MultiDiskOptionsView
<T
> {
333 phantom
: PhantomData
<T
>,
336 impl<T
: View
> MultiDiskOptionsView
<T
> {
337 fn new(avail_disks
: &[Disk
], selected_disks
: &[usize], options_view
: T
) -> Self {
338 let mut selectable_disks
= avail_disks
340 .map(|d
| (d
.to_string(), Some(d
.clone())))
341 .collect
::<Vec
<(String
, Option
<Disk
>)>>();
343 selectable_disks
.push(("-- do not use --".to_owned(), None
));
345 let mut disk_form
= FormView
::new();
346 for (i
, _
) in avail_disks
.iter().enumerate() {
348 &format
!("Harddisk {i}"),
351 .with_all(selectable_disks
.clone())
352 .selected(selected_disks
[i
]),
356 let mut disk_select_view
= LinearLayout
::vertical()
357 .child(TextView
::new("Disk setup").center())
359 .child(ScrollView
::new(disk_form
.with_name("multidisk-disk-form")));
361 if avail_disks
.len() > 3 {
362 let do_not_use_index
= selectable_disks
.len() - 1;
363 let deselect_all_button
= Button
::new("Deselect all", move |siv
| {
364 siv
.call_on_name("multidisk-disk-form", |view
: &mut FormView
| {
365 view
.call_on_childs(&|v
: &mut SelectView
<Option
<Disk
>>| {
366 // As there is no .on_select() callback defined on the
367 // SelectView's, the returned callback here can be safely
369 v
.set_selection(do_not_use_index
);
374 disk_select_view
.add_child(PaddedView
::lrtb(
379 LinearLayout
::horizontal()
380 .child(DummyView
.full_width())
381 .child(deselect_all_button
),
385 let options_view
= LinearLayout
::vertical()
386 .child(TextView
::new("Advanced options").center())
388 .child(options_view
);
390 let view
= LinearLayout
::horizontal()
391 .child(disk_select_view
)
392 .child(DummyView
.fixed_width(3))
393 .child(options_view
);
396 view
: LinearLayout
::vertical().child(view
),
397 phantom
: PhantomData
,
401 fn top_panel(mut self, view
: impl View
) -> Self {
402 if self.has_top_panel() {
403 self.view
.remove_child(0);
406 self.view
.insert_child(0, Panel
::new(view
));
411 /// This function returns a tuple of vectors. The first vector contains the currently selected
412 /// disks in order of their selection slot. Empty slots are filtered out. The second vector
413 /// contains indices of each slot's selection, which enables us to restore the selection even
416 fn get_disks_and_selection(&mut self) -> Option
<(Vec
<Disk
>, Vec
<usize>)> {
417 let mut disks
= vec
![];
418 let view_top_index
= usize::from(self.has_top_panel());
422 .get_child_mut(view_top_index
)?
423 .downcast_mut
::<LinearLayout
>()?
425 .downcast_mut
::<LinearLayout
>()?
427 .downcast_mut
::<ScrollView
<NamedView
<FormView
>>>()?
431 let mut selected_disks
= Vec
::new();
433 for i
in 0..disk_form
.len() {
434 let disk
= disk_form
.get_value
::<SelectView
<Option
<Disk
>>, _
>(i
)?
;
436 // `None` means no disk was selected for this slot
437 if let Some(disk
) = disk
{
443 .get_child
::<SelectView
<Option
<Disk
>>>(i
)?
448 Some((disks
, selected_disks
))
451 fn inner_mut(&mut self) -> Option
<&mut T
> {
452 let view_top_index
= usize::from(self.has_top_panel());
455 .get_child_mut(view_top_index
)?
456 .downcast_mut
::<LinearLayout
>()?
458 .downcast_mut
::<LinearLayout
>()?
463 fn has_top_panel(&self) -> bool
{
464 // The root view should only ever have one or two children
465 assert
!([1, 2].contains(&self.view
.len()));
471 impl<T
: '
static> ViewWrapper
for MultiDiskOptionsView
<T
> {
472 cursive
::wrap_impl
!(self.view
: LinearLayout
);
475 struct BtrfsBootdiskOptionsView
{
476 view
: MultiDiskOptionsView
<FormView
>,
479 impl BtrfsBootdiskOptionsView
{
480 fn new(disks
: &[Disk
], options
: &BtrfsBootdiskOptions
) -> Self {
481 let view
= MultiDiskOptionsView
::new(
483 &options
.selected_disks
,
484 FormView
::new().child("hdsize", DiskSizeEditView
::new().content(options
.disk_size
)),
486 .top_panel(TextView
::new("Btrfs integration is a technology preview!").center());
491 fn new_with_defaults(disks
: &[Disk
]) -> Self {
492 Self::new(disks
, &BtrfsBootdiskOptions
::defaults_from(disks
))
495 fn get_values(&mut self) -> Option
<(Vec
<Disk
>, BtrfsBootdiskOptions
)> {
496 let (disks
, selected_disks
) = self.view
.get_disks_and_selection()?
;
497 let disk_size
= self.view
.inner_mut()?
.get_value
::<DiskSizeEditView
, _
>(0)?
;
501 BtrfsBootdiskOptions
{
509 impl ViewWrapper
for BtrfsBootdiskOptionsView
{
510 cursive
::wrap_impl
!(self.view
: MultiDiskOptionsView
<FormView
>);
513 struct ZfsBootdiskOptionsView
{
514 view
: MultiDiskOptionsView
<FormView
>,
517 impl ZfsBootdiskOptionsView
{
518 // TODO: Re-apply previous disk selection from `options` correctly
519 fn new(disks
: &[Disk
], options
: &ZfsBootdiskOptions
) -> Self {
520 let inner
= FormView
::new()
521 .child("ashift", IntegerEditView
::new().content(options
.ashift
))
526 .with_all(ZFS_COMPRESS_OPTIONS
.iter().map(|o
| (o
.to_string(), *o
)))
530 .position(|o
| *o
== options
.compress
)
531 .unwrap_or_default(),
538 .with_all(ZFS_CHECKSUM_OPTIONS
.iter().map(|o
| (o
.to_string(), *o
)))
542 .position(|o
| *o
== options
.checksum
)
543 .unwrap_or_default(),
546 .child("copies", IntegerEditView
::new().content(options
.copies
))
547 .child("hdsize", DiskSizeEditView
::new().content(options
.disk_size
));
549 let view
= MultiDiskOptionsView
::new(disks
, &options
.selected_disks
, inner
)
550 .top_panel(TextView
::new(
551 "ZFS is not compatible with hardware RAID controllers, for details see the documentation."
557 fn new_with_defaults(runinfo
: &RuntimeInfo
, product_conf
: &ProductConfig
) -> Self {
560 &ZfsBootdiskOptions
::defaults_from(runinfo
, product_conf
),
564 fn get_values(&mut self) -> Option
<(Vec
<Disk
>, ZfsBootdiskOptions
)> {
565 let (disks
, selected_disks
) = self.view
.get_disks_and_selection()?
;
566 let view
= self.view
.inner_mut()?
;
568 let ashift
= view
.get_value
::<IntegerEditView
, _
>(0)?
;
569 let compress
= view
.get_value
::<SelectView
<_
>, _
>(1)?
;
570 let checksum
= view
.get_value
::<SelectView
<_
>, _
>(2)?
;
571 let copies
= view
.get_value
::<IntegerEditView
, _
>(3)?
;
572 let disk_size
= view
.get_value
::<DiskSizeEditView
, _
>(4)?
;
581 arc_max
: 0, // use built-in ZFS default value
589 impl ViewWrapper
for ZfsBootdiskOptionsView
{
590 cursive
::wrap_impl
!(self.view
: MultiDiskOptionsView
<FormView
>);
593 fn advanced_options_view(
595 options
: Rc
<RefCell
<BootdiskOptions
>>,
596 product_conf
: ProductConfig
,
598 Dialog
::around(AdvancedBootdiskOptionsView
::new(
600 &(*options
).borrow(),
603 .title("Advanced bootdisk options")
605 let options_ref
= options
.clone();
608 .call_on_name("advanced-bootdisk-options-dialog", |view
: &mut Dialog
| {
609 view
.get_content_mut()
610 .downcast_mut
::<AdvancedBootdiskOptionsView
>()
611 .map(AdvancedBootdiskOptionsView
::get_values
)
615 let options
= match options
{
616 Some(Ok(options
)) => options
,
618 siv
.add_layer(Dialog
::info(err
));
622 siv
.add_layer(Dialog
::info("Failed to retrieve bootdisk options view"));
627 if let Err(duplicate
) = check_for_duplicate_disks(&options
.disks
) {
628 siv
.add_layer(Dialog
::info(format
!(
629 "Cannot select same disk twice: {duplicate}"
635 *(*options_ref
).borrow_mut() = options
;
638 .with_name("advanced-bootdisk-options-dialog")