]> git.proxmox.com Git - pve-installer.git/blobdiff - proxmox-tui-installer/src/views/bootdisk.rs
tui: bootdisk: refactor Rc<RefCell<..>> type into custom type
[pve-installer.git] / proxmox-tui-installer / src / views / bootdisk.rs
index 775ffa4d6512c5c411ffaa3cc700d0188b726fc2..6309d7450bed398384fa2486dafab4042ad50ce8 100644 (file)
@@ -3,42 +3,71 @@ use std::{cell::RefCell, marker::PhantomData, rc::Rc};
 use cursive::{
     view::{Nameable, Resizable, ViewWrapper},
     views::{
-        Button, Dialog, DummyView, LinearLayout, NamedView, Panel, ScrollView, SelectView, TextView,
+        Button, Dialog, DummyView, LinearLayout, NamedView, PaddedView, Panel, ScrollView,
+        SelectView, TextView,
     },
     Cursive, View,
 };
 
 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},
 };
 
+/// 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(),
+                    ));
                 }
             }));
 
@@ -47,9 +76,15 @@ impl BootdiskOptionsView {
             .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,
         }
     }
 
@@ -63,11 +98,12 @@ impl BootdiskOptionsView {
                 .and_then(|v| v.downcast_mut::<NamedView<FormView>>())
                 .map(NamedView::<FormView>::get_mut)
                 .and_then(|v| v.get_value::<SelectView<Disk>, _>(0))
-                .ok_or("failed to retrieve filesystem type")?;
+                .ok_or("failed to retrieve bootdisk")?;
 
             options.disks = vec![disk];
         }
 
+        check_disks_4kn_legacy_boot(self.boot_type, &options.disks)?;
         Ok(options)
     }
 }
@@ -81,20 +117,26 @@ struct AdvancedBootdiskOptionsView {
 }
 
 impl AdvancedBootdiskOptionsView {
-    fn new(disks: &[Disk], options: &BootdiskOptions) -> Self {
+    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()
-            .with_all(FS_TYPES.iter().map(|t| (t.to_string(), *t)))
+            .with_all(
+                FS_TYPES
+                    .iter()
+                    .filter(filter_btrfs)
+                    .map(|t| (t.to_string(), *t)),
+            )
             .selected(
                 FS_TYPES
                     .iter()
+                    .filter(filter_btrfs)
                     .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())
@@ -102,36 +144,41 @@ impl AdvancedBootdiskOptionsView {
             .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[0]),
-                    )),
-                    FsType::Btrfs(_) => view.add_child(BtrfsBootdiskOptionsView::new(
-                        disks,
-                        &BtrfsBootdiskOptions::defaults_from(&disks[0]),
+                    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))
+                    }
                 }
             }
         });
@@ -144,7 +191,7 @@ impl AdvancedBootdiskOptionsView {
                         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())),
@@ -152,39 +199,60 @@ impl AdvancedBootdiskOptionsView {
         );
     }
 
-    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())
         }
     }
 }
@@ -195,10 +263,13 @@ impl ViewWrapper for AdvancedBootdiskOptionsView {
 
 struct LvmBootdiskOptionsView {
     view: FormView,
+    has_extra_fields: bool,
 }
 
 impl LvmBootdiskOptionsView {
-    fn new(options: &LvmBootdiskOptions) -> Self {
+    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(
@@ -211,11 +282,13 @@ impl LvmBootdiskOptionsView {
                 "Swap size",
                 DiskSizeEditView::new_emptyable().content_maybe(options.swap_size),
             )
-            .child(
+            .child_conditional(
+                show_extra_fields,
                 "Maximum root volume size",
                 DiskSizeEditView::new_emptyable().content_maybe(options.max_root_size),
             )
-            .child(
+            .child_conditional(
+                show_extra_fields,
                 "Maximum data volume size",
                 DiskSizeEditView::new_emptyable().content_maybe(options.max_data_size),
             )
@@ -224,16 +297,34 @@ impl LvmBootdiskOptionsView {
                 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 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),
-            max_root_size: self.view.get_value::<DiskSizeEditView, _>(2),
-            max_data_size: self.view.get_value::<DiskSizeEditView, _>(3),
-            min_lvm_free: self.view.get_value::<DiskSizeEditView, _>(4),
+            max_root_size,
+            max_data_size,
+            min_lvm_free: self.view.get_value::<DiskSizeEditView, _>(min_lvm_free_id),
         })
     }
 }
@@ -248,7 +339,7 @@ struct MultiDiskOptionsView<T> {
 }
 
 impl<T: View> MultiDiskOptionsView<T> {
-    fn new(avail_disks: &[Disk], options_view: T) -> Self {
+    fn new(avail_disks: &[Disk], selected_disks: &[usize], options_view: T) -> Self {
         let mut selectable_disks = avail_disks
             .iter()
             .map(|d| (d.to_string(), Some(d.clone())))
@@ -257,20 +348,44 @@ impl<T: View> MultiDiskOptionsView<T> {
         selectable_disks.push(("-- do not use --".to_owned(), None));
 
         let mut disk_form = FormView::new();
-        for i in 0..avail_disks.len() {
+        for (i, _) in avail_disks.iter().enumerate() {
             disk_form.add_child(
                 &format!("Harddisk {i}"),
                 SelectView::new()
                     .popup()
                     .with_all(selectable_disks.clone())
-                    .selected(i),
+                    .selected(selected_disks[i]),
             );
         }
 
-        let disk_select_view = LinearLayout::vertical()
+        let mut disk_select_view = LinearLayout::vertical()
             .child(TextView::new("Disk setup").center())
             .child(DummyView)
-            .child(ScrollView::new(disk_form));
+            .child(ScrollView::new(disk_form.with_name("multidisk-disk-form")));
+
+        if avail_disks.len() > 3 {
+            let do_not_use_index = selectable_disks.len() - 1;
+            let deselect_all_button = Button::new("Deselect all", move |siv| {
+                siv.call_on_name("multidisk-disk-form", |view: &mut FormView| {
+                    view.call_on_childs(&|v: &mut SelectView<Option<Disk>>| {
+                        // As there is no .on_select() callback defined on the
+                        // SelectView's, the returned callback here can be safely
+                        // ignored.
+                        v.set_selection(do_not_use_index);
+                    });
+                });
+            });
+
+            disk_select_view.add_child(PaddedView::lrtb(
+                0,
+                0,
+                1,
+                0,
+                LinearLayout::horizontal()
+                    .child(DummyView.full_width())
+                    .child(deselect_all_button),
+            ));
+        }
 
         let options_view = LinearLayout::vertical()
             .child(TextView::new("Advanced options").center())
@@ -297,19 +412,28 @@ impl<T: View> MultiDiskOptionsView<T> {
         self
     }
 
-    fn get_disks(&mut self) -> Option<Vec<Disk>> {
+    ///
+    /// This function returns a tuple of vectors. The first vector contains the currently selected
+    /// disks in order of their selection slot. Empty slots are filtered out. The second vector
+    /// contains indices of each slot's selection, which enables us to restore the selection even
+    /// for empty slots.
+    ///
+    fn get_disks_and_selection(&mut self) -> Option<(Vec<Disk>, Vec<usize>)> {
         let mut disks = vec![];
         let view_top_index = usize::from(self.has_top_panel());
 
         let disk_form = self
             .view
-            .get_child(view_top_index)?
-            .downcast_ref::<LinearLayout>()?
-            .get_child(0)?
-            .downcast_ref::<LinearLayout>()?
-            .get_child(2)?
-            .downcast_ref::<ScrollView<FormView>>()?
-            .get_inner();
+            .get_child_mut(view_top_index)?
+            .downcast_mut::<LinearLayout>()?
+            .get_child_mut(0)?
+            .downcast_mut::<LinearLayout>()?
+            .get_child_mut(2)?
+            .downcast_mut::<ScrollView<NamedView<FormView>>>()?
+            .get_inner_mut()
+            .get_mut();
+
+        let mut selected_disks = Vec::new();
 
         for i in 0..disk_form.len() {
             let disk = disk_form.get_value::<SelectView<Option<Disk>>, _>(i)?;
@@ -318,9 +442,15 @@ impl<T: View> MultiDiskOptionsView<T> {
             if let Some(disk) = disk {
                 disks.push(disk);
             }
+
+            selected_disks.push(
+                disk_form
+                    .get_child::<SelectView<Option<Disk>>>(i)?
+                    .selected_id()?,
+            );
         }
 
-        Some(disks)
+        Some((disks, selected_disks))
     }
 
     fn inner_mut(&mut self) -> Option<&mut T> {
@@ -352,10 +482,10 @@ struct BtrfsBootdiskOptionsView {
 }
 
 impl BtrfsBootdiskOptionsView {
-    // TODO: Re-apply previous disk selection from `options` correctly
     fn new(disks: &[Disk], options: &BtrfsBootdiskOptions) -> Self {
         let view = MultiDiskOptionsView::new(
             disks,
+            &options.selected_disks,
             FormView::new().child("hdsize", DiskSizeEditView::new().content(options.disk_size)),
         )
         .top_panel(TextView::new("Btrfs integration is a technology preview!").center());
@@ -363,11 +493,21 @@ impl BtrfsBootdiskOptionsView {
         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 = self.view.get_disks()?;
+        let (disks, selected_disks) = self.view.get_disks_and_selection()?;
         let disk_size = self.view.inner_mut()?.get_value::<DiskSizeEditView, _>(0)?;
 
-        Some((disks, BtrfsBootdiskOptions { disk_size }))
+        Some((
+            disks,
+            BtrfsBootdiskOptions {
+                disk_size,
+                selected_disks,
+            },
+        ))
     }
 }
 
@@ -381,7 +521,13 @@ struct ZfsBootdiskOptionsView {
 
 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(
@@ -409,9 +555,16 @@ impl ZfsBootdiskOptionsView {
                     ),
             )
             .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, 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());
@@ -419,15 +572,32 @@ impl ZfsBootdiskOptionsView {
         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 = self.view.get_disks()?;
+        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,
@@ -436,7 +606,9 @@ impl ZfsBootdiskOptionsView {
                 compress,
                 checksum,
                 copies,
+                arc_max,
                 disk_size,
+                selected_disks,
             },
         ))
     }
@@ -446,27 +618,49 @@ impl ViewWrapper for ZfsBootdiskOptionsView {
     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();
 
-            siv.pop_layer();
-            if let Some(options) = options {
-                *(*options_ref).borrow_mut() = options;
+            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}"
+                )));
+                return;
             }
+
+            siv.pop_layer();
+            *(*options_ref).borrow_mut() = options;
         }
     })
     .with_name("advanced-bootdisk-options-dialog")