]> git.proxmox.com Git - pve-installer.git/blob - proxmox-tui-installer/src/views/bootdisk.rs
tui: improve bootdisk dialog error handling
[pve-installer.git] / proxmox-tui-installer / src / views / bootdisk.rs
1 use std::{cell::RefCell, collections::HashSet, marker::PhantomData, rc::Rc};
2
3 use cursive::{
4 view::{Nameable, Resizable, ViewWrapper},
5 views::{
6 Button, Dialog, DummyView, LinearLayout, NamedView, PaddedView, Panel, ScrollView,
7 SelectView, TextView,
8 },
9 Cursive, View,
10 };
11
12 use super::{DiskSizeEditView, FormView, IntegerEditView};
13 use crate::options::{
14 AdvancedBootdiskOptions, BootdiskOptions, BtrfsBootdiskOptions, Disk, FsType,
15 LvmBootdiskOptions, ZfsBootdiskOptions, FS_TYPES, ZFS_CHECKSUM_OPTIONS, ZFS_COMPRESS_OPTIONS,
16 };
17 use crate::setup::ProxmoxProduct;
18
19 pub struct BootdiskOptionsView {
20 view: LinearLayout,
21 advanced_options: Rc<RefCell<BootdiskOptions>>,
22 }
23
24 impl BootdiskOptionsView {
25 pub fn new(disks: &[Disk], options: &BootdiskOptions) -> Self {
26 let bootdisk_form = FormView::new()
27 .child(
28 "Target harddisk",
29 SelectView::new()
30 .popup()
31 .with_all(disks.iter().map(|d| (d.to_string(), d.clone()))),
32 )
33 .with_name("bootdisk-options-target-disk");
34
35 let advanced_options = Rc::new(RefCell::new(options.clone()));
36
37 let advanced_button = LinearLayout::horizontal()
38 .child(DummyView.full_width())
39 .child(Button::new("Advanced options", {
40 let disks = disks.to_owned();
41 let options = advanced_options.clone();
42 move |siv| {
43 siv.add_layer(advanced_options_view(&disks, options.clone()));
44 }
45 }));
46
47 let view = LinearLayout::vertical()
48 .child(bootdisk_form)
49 .child(DummyView)
50 .child(advanced_button);
51
52 Self {
53 view,
54 advanced_options,
55 }
56 }
57
58 pub fn get_values(&mut self) -> Result<BootdiskOptions, String> {
59 let mut options = (*self.advanced_options).clone().into_inner();
60
61 if [FsType::Ext4, FsType::Xfs].contains(&options.fstype) {
62 let disk = self
63 .view
64 .get_child_mut(0)
65 .and_then(|v| v.downcast_mut::<NamedView<FormView>>())
66 .map(NamedView::<FormView>::get_mut)
67 .and_then(|v| v.get_value::<SelectView<Disk>, _>(0))
68 .ok_or("failed to retrieve bootdisk")?;
69
70 options.disks = vec![disk];
71 }
72
73 Ok(options)
74 }
75 }
76
77 impl ViewWrapper for BootdiskOptionsView {
78 cursive::wrap_impl!(self.view: LinearLayout);
79 }
80
81 struct AdvancedBootdiskOptionsView {
82 view: LinearLayout,
83 }
84
85 impl AdvancedBootdiskOptionsView {
86 fn new(disks: &[Disk], options: &BootdiskOptions) -> Self {
87 let enable_btrfs = crate::setup_info().config.enable_btrfs;
88
89 let filter_btrfs = |fstype: &&FsType| -> bool { enable_btrfs || !fstype.is_btrfs() };
90
91 let fstype_select = SelectView::new()
92 .popup()
93 .with_all(
94 FS_TYPES
95 .iter()
96 .filter(filter_btrfs)
97 .map(|t| (t.to_string(), *t)),
98 )
99 .selected(
100 FS_TYPES
101 .iter()
102 .filter(filter_btrfs)
103 .position(|t| *t == options.fstype)
104 .unwrap_or_default(),
105 )
106 .on_submit({
107 let disks = disks.to_owned();
108 move |siv, fstype| Self::fstype_on_submit(siv, &disks, fstype)
109 });
110
111 let mut view = LinearLayout::vertical()
112 .child(DummyView.full_width())
113 .child(FormView::new().child("Filesystem", fstype_select))
114 .child(DummyView.full_width());
115
116 match &options.advanced {
117 AdvancedBootdiskOptions::Lvm(lvm) => view.add_child(LvmBootdiskOptionsView::new(lvm)),
118 AdvancedBootdiskOptions::Zfs(zfs) => {
119 view.add_child(ZfsBootdiskOptionsView::new(disks, zfs))
120 }
121 AdvancedBootdiskOptions::Btrfs(btrfs) => {
122 view.add_child(BtrfsBootdiskOptionsView::new(disks, btrfs))
123 }
124 };
125
126 Self { view }
127 }
128
129 fn fstype_on_submit(siv: &mut Cursive, disks: &[Disk], fstype: &FsType) {
130 siv.call_on_name("advanced-bootdisk-options-dialog", |view: &mut Dialog| {
131 if let Some(AdvancedBootdiskOptionsView { view }) =
132 view.get_content_mut().downcast_mut()
133 {
134 view.remove_child(3);
135 match fstype {
136 FsType::Ext4 | FsType::Xfs => view.add_child(LvmBootdiskOptionsView::new(
137 &LvmBootdiskOptions::defaults_from(&disks[0]),
138 )),
139 FsType::Zfs(_) => view.add_child(ZfsBootdiskOptionsView::new(
140 disks,
141 &ZfsBootdiskOptions::defaults_from(disks),
142 )),
143 FsType::Btrfs(_) => view.add_child(BtrfsBootdiskOptionsView::new(
144 disks,
145 &BtrfsBootdiskOptions::defaults_from(disks),
146 )),
147 }
148 }
149 });
150
151 siv.call_on_name(
152 "bootdisk-options-target-disk",
153 |view: &mut FormView| match fstype {
154 FsType::Ext4 | FsType::Xfs => {
155 view.replace_child(
156 0,
157 SelectView::new()
158 .popup()
159 .with_all(disks.iter().map(|d| (d.to_string(), d.clone()))),
160 );
161 }
162 other => view.replace_child(0, TextView::new(other.to_string())),
163 },
164 );
165 }
166
167 fn get_values(&mut self) -> Result<BootdiskOptions, String> {
168 let fstype = self
169 .view
170 .get_child(1)
171 .and_then(|v| v.downcast_ref::<FormView>())
172 .and_then(|v| v.get_value::<SelectView<FsType>, _>(0))
173 .ok_or("Failed to retrieve filesystem type".to_owned())?;
174
175 let advanced = self
176 .view
177 .get_child_mut(3)
178 .ok_or("Failed to retrieve advanced bootdisk options view".to_owned())?;
179
180 if let Some(view) = advanced.downcast_mut::<LvmBootdiskOptionsView>() {
181 let advanced = view
182 .get_values()
183 .map(AdvancedBootdiskOptions::Lvm)
184 .ok_or("Failed to retrieve advanced bootdisk options")?;
185
186 Ok(BootdiskOptions {
187 disks: vec![],
188 fstype,
189 advanced,
190 })
191 } else if let Some(view) = advanced.downcast_mut::<ZfsBootdiskOptionsView>() {
192 let (disks, advanced) = view
193 .get_values()
194 .ok_or("Failed to retrieve advanced bootdisk options")?;
195
196 Ok(BootdiskOptions {
197 disks,
198 fstype,
199 advanced: AdvancedBootdiskOptions::Zfs(advanced),
200 })
201 } else if let Some(view) = advanced.downcast_mut::<BtrfsBootdiskOptionsView>() {
202 let (disks, advanced) = view
203 .get_values()
204 .ok_or("Failed to retrieve advanced bootdisk options")?;
205
206 Ok(BootdiskOptions {
207 disks,
208 fstype,
209 advanced: AdvancedBootdiskOptions::Btrfs(advanced),
210 })
211 } else {
212 Err("Invalid bootdisk view state".to_owned())
213 }
214 }
215 }
216
217 impl ViewWrapper for AdvancedBootdiskOptionsView {
218 cursive::wrap_impl!(self.view: LinearLayout);
219 }
220
221 struct LvmBootdiskOptionsView {
222 view: FormView,
223 }
224
225 impl LvmBootdiskOptionsView {
226 fn new(options: &LvmBootdiskOptions) -> Self {
227 let is_pve = crate::setup_info().config.product == ProxmoxProduct::PVE;
228 // TODO: Set maximum accordingly to disk size
229 let view = FormView::new()
230 .child(
231 "Total size",
232 DiskSizeEditView::new()
233 .content(options.total_size)
234 .max_value(options.total_size),
235 )
236 .child(
237 "Swap size",
238 DiskSizeEditView::new_emptyable().content_maybe(options.swap_size),
239 )
240 .child_conditional(
241 is_pve,
242 "Maximum root volume size",
243 DiskSizeEditView::new_emptyable().content_maybe(options.max_root_size),
244 )
245 .child_conditional(
246 is_pve,
247 "Maximum data volume size",
248 DiskSizeEditView::new_emptyable().content_maybe(options.max_data_size),
249 )
250 .child(
251 "Minimum free LVM space",
252 DiskSizeEditView::new_emptyable().content_maybe(options.min_lvm_free),
253 );
254
255 Self { view }
256 }
257
258 fn get_values(&mut self) -> Option<LvmBootdiskOptions> {
259 let is_pve = crate::setup_info().config.product == ProxmoxProduct::PVE;
260 let min_lvm_free_id = if is_pve { 4 } else { 2 };
261 let max_root_size = if is_pve {
262 self.view.get_value::<DiskSizeEditView, _>(2)
263 } else {
264 None
265 };
266 let max_data_size = if is_pve {
267 self.view.get_value::<DiskSizeEditView, _>(3)
268 } else {
269 None
270 };
271 Some(LvmBootdiskOptions {
272 total_size: self.view.get_value::<DiskSizeEditView, _>(0)?,
273 swap_size: self.view.get_value::<DiskSizeEditView, _>(1),
274 max_root_size,
275 max_data_size,
276 min_lvm_free: self.view.get_value::<DiskSizeEditView, _>(min_lvm_free_id),
277 })
278 }
279 }
280
281 impl ViewWrapper for LvmBootdiskOptionsView {
282 cursive::wrap_impl!(self.view: FormView);
283 }
284
285 struct MultiDiskOptionsView<T> {
286 view: LinearLayout,
287 phantom: PhantomData<T>,
288 }
289
290 impl<T: View> MultiDiskOptionsView<T> {
291 fn new(avail_disks: &[Disk], selected_disks: &[usize], options_view: T) -> Self {
292 let mut selectable_disks = avail_disks
293 .iter()
294 .map(|d| (d.to_string(), Some(d.clone())))
295 .collect::<Vec<(String, Option<Disk>)>>();
296
297 selectable_disks.push(("-- do not use --".to_owned(), None));
298
299 let mut disk_form = FormView::new();
300 for (i, _) in avail_disks.iter().enumerate() {
301 disk_form.add_child(
302 &format!("Harddisk {i}"),
303 SelectView::new()
304 .popup()
305 .with_all(selectable_disks.clone())
306 .selected(selected_disks[i]),
307 );
308 }
309
310 let mut disk_select_view = LinearLayout::vertical()
311 .child(TextView::new("Disk setup").center())
312 .child(DummyView)
313 .child(ScrollView::new(disk_form.with_name("multidisk-disk-form")));
314
315 if avail_disks.len() > 3 {
316 let do_not_use_index = selectable_disks.len() - 1;
317 let deselect_all_button = Button::new("Deselect all", move |siv| {
318 siv.call_on_name("multidisk-disk-form", |view: &mut FormView| {
319 view.call_on_childs(&|v: &mut SelectView<Option<Disk>>| {
320 // As there is no .on_select() callback defined on the
321 // SelectView's, the returned callback here can be safely
322 // ignored.
323 v.set_selection(do_not_use_index);
324 });
325 });
326 });
327
328 disk_select_view.add_child(PaddedView::lrtb(
329 0,
330 0,
331 1,
332 0,
333 LinearLayout::horizontal()
334 .child(DummyView.full_width())
335 .child(deselect_all_button),
336 ));
337 }
338
339 let options_view = LinearLayout::vertical()
340 .child(TextView::new("Advanced options").center())
341 .child(DummyView)
342 .child(options_view);
343
344 let view = LinearLayout::horizontal()
345 .child(disk_select_view)
346 .child(DummyView.fixed_width(3))
347 .child(options_view);
348
349 Self {
350 view: LinearLayout::vertical().child(view),
351 phantom: PhantomData,
352 }
353 }
354
355 fn top_panel(mut self, view: impl View) -> Self {
356 if self.has_top_panel() {
357 self.view.remove_child(0);
358 }
359
360 self.view.insert_child(0, Panel::new(view));
361 self
362 }
363
364 ///
365 /// This function returns a tuple of vectors. The first vector contains the currently selected
366 /// disks in order of their selection slot. Empty slots are filtered out. The second vector
367 /// contains indices of each slot's selection, which enables us to restore the selection even
368 /// for empty slots.
369 ///
370 fn get_disks_and_selection(&mut self) -> Option<(Vec<Disk>, Vec<usize>)> {
371 let mut disks = vec![];
372 let view_top_index = usize::from(self.has_top_panel());
373
374 let disk_form = self
375 .view
376 .get_child_mut(view_top_index)?
377 .downcast_mut::<LinearLayout>()?
378 .get_child_mut(0)?
379 .downcast_mut::<LinearLayout>()?
380 .get_child_mut(2)?
381 .downcast_mut::<ScrollView<NamedView<FormView>>>()?
382 .get_inner_mut()
383 .get_mut();
384
385 let mut selected_disks = Vec::new();
386
387 for i in 0..disk_form.len() {
388 let disk = disk_form.get_value::<SelectView<Option<Disk>>, _>(i)?;
389
390 // `None` means no disk was selected for this slot
391 if let Some(disk) = disk {
392 disks.push(disk);
393 }
394
395 selected_disks.push(
396 disk_form
397 .get_child::<SelectView<Option<Disk>>>(i)?
398 .selected_id()?,
399 );
400 }
401
402 Some((disks, selected_disks))
403 }
404
405 fn inner_mut(&mut self) -> Option<&mut T> {
406 let view_top_index = usize::from(self.has_top_panel());
407
408 self.view
409 .get_child_mut(view_top_index)?
410 .downcast_mut::<LinearLayout>()?
411 .get_child_mut(2)?
412 .downcast_mut::<LinearLayout>()?
413 .get_child_mut(2)?
414 .downcast_mut::<T>()
415 }
416
417 fn has_top_panel(&self) -> bool {
418 // The root view should only ever have one or two children
419 assert!([1, 2].contains(&self.view.len()));
420
421 self.view.len() == 2
422 }
423 }
424
425 impl<T: 'static> ViewWrapper for MultiDiskOptionsView<T> {
426 cursive::wrap_impl!(self.view: LinearLayout);
427 }
428
429 struct BtrfsBootdiskOptionsView {
430 view: MultiDiskOptionsView<FormView>,
431 }
432
433 impl BtrfsBootdiskOptionsView {
434 fn new(disks: &[Disk], options: &BtrfsBootdiskOptions) -> Self {
435 let view = MultiDiskOptionsView::new(
436 disks,
437 &options.selected_disks,
438 FormView::new().child("hdsize", DiskSizeEditView::new().content(options.disk_size)),
439 )
440 .top_panel(TextView::new("Btrfs integration is a technology preview!").center());
441
442 Self { view }
443 }
444
445 fn get_values(&mut self) -> Option<(Vec<Disk>, BtrfsBootdiskOptions)> {
446 let (disks, selected_disks) = self.view.get_disks_and_selection()?;
447 let disk_size = self.view.inner_mut()?.get_value::<DiskSizeEditView, _>(0)?;
448
449 Some((
450 disks,
451 BtrfsBootdiskOptions {
452 disk_size,
453 selected_disks,
454 },
455 ))
456 }
457 }
458
459 impl ViewWrapper for BtrfsBootdiskOptionsView {
460 cursive::wrap_impl!(self.view: MultiDiskOptionsView<FormView>);
461 }
462
463 struct ZfsBootdiskOptionsView {
464 view: MultiDiskOptionsView<FormView>,
465 }
466
467 impl ZfsBootdiskOptionsView {
468 // TODO: Re-apply previous disk selection from `options` correctly
469 fn new(disks: &[Disk], options: &ZfsBootdiskOptions) -> Self {
470 let inner = FormView::new()
471 .child("ashift", IntegerEditView::new().content(options.ashift))
472 .child(
473 "compress",
474 SelectView::new()
475 .popup()
476 .with_all(ZFS_COMPRESS_OPTIONS.iter().map(|o| (o.to_string(), *o)))
477 .selected(
478 ZFS_COMPRESS_OPTIONS
479 .iter()
480 .position(|o| *o == options.compress)
481 .unwrap_or_default(),
482 ),
483 )
484 .child(
485 "checksum",
486 SelectView::new()
487 .popup()
488 .with_all(ZFS_CHECKSUM_OPTIONS.iter().map(|o| (o.to_string(), *o)))
489 .selected(
490 ZFS_CHECKSUM_OPTIONS
491 .iter()
492 .position(|o| *o == options.checksum)
493 .unwrap_or_default(),
494 ),
495 )
496 .child("copies", IntegerEditView::new().content(options.copies))
497 .child("hdsize", DiskSizeEditView::new().content(options.disk_size));
498
499 let view = MultiDiskOptionsView::new(disks, &options.selected_disks, inner)
500 .top_panel(TextView::new(
501 "ZFS is not compatible with hardware RAID controllers, for details see the documentation."
502 ).center());
503
504 Self { view }
505 }
506
507 fn get_values(&mut self) -> Option<(Vec<Disk>, ZfsBootdiskOptions)> {
508 let (disks, selected_disks) = self.view.get_disks_and_selection()?;
509 let view = self.view.inner_mut()?;
510
511 let ashift = view.get_value::<IntegerEditView, _>(0)?;
512 let compress = view.get_value::<SelectView<_>, _>(1)?;
513 let checksum = view.get_value::<SelectView<_>, _>(2)?;
514 let copies = view.get_value::<IntegerEditView, _>(3)?;
515 let disk_size = view.get_value::<DiskSizeEditView, _>(4)?;
516
517 Some((
518 disks,
519 ZfsBootdiskOptions {
520 ashift,
521 compress,
522 checksum,
523 copies,
524 disk_size,
525 selected_disks,
526 },
527 ))
528 }
529 }
530
531 impl ViewWrapper for ZfsBootdiskOptionsView {
532 cursive::wrap_impl!(self.view: MultiDiskOptionsView<FormView>);
533 }
534
535 fn advanced_options_view(disks: &[Disk], options: Rc<RefCell<BootdiskOptions>>) -> impl View {
536 Dialog::around(AdvancedBootdiskOptionsView::new(
537 disks,
538 &(*options).borrow(),
539 ))
540 .title("Advanced bootdisk options")
541 .button("Ok", {
542 let options_ref = options.clone();
543 move |siv| {
544 let options = siv
545 .call_on_name("advanced-bootdisk-options-dialog", |view: &mut Dialog| {
546 view.get_content_mut()
547 .downcast_mut()
548 .map(AdvancedBootdiskOptionsView::get_values)
549 })
550 .flatten();
551
552 let options = match options {
553 Some(Ok(options)) => options,
554 Some(Err(err)) => {
555 siv.add_layer(Dialog::info(err));
556 return;
557 }
558 None => {
559 siv.add_layer(Dialog::info("Failed to retrieve bootdisk options view"));
560 return;
561 }
562 };
563
564 if let Err(duplicate) = check_for_duplicate_disks(&options.disks) {
565 siv.add_layer(Dialog::info(format!(
566 "Cannot select same disk twice: {duplicate}"
567 )));
568 return;
569 }
570
571 siv.pop_layer();
572 *(*options_ref).borrow_mut() = options;
573 }
574 })
575 .with_name("advanced-bootdisk-options-dialog")
576 .max_size((120, 40))
577 }
578
579 /// Checks a list of disks for duplicate entries, using their index as key.
580 ///
581 /// # Arguments
582 ///
583 /// * `disks` - A list of disks to check for duplicates.
584 fn check_for_duplicate_disks(disks: &[Disk]) -> Result<(), &Disk> {
585 let mut set = HashSet::new();
586
587 for disk in disks {
588 if !set.insert(&disk.index) {
589 return Err(disk);
590 }
591 }
592
593 Ok(())
594 }