]> git.proxmox.com Git - pve-installer.git/blob - proxmox-tui-installer/src/views/bootdisk.rs
tui: simplify duplicate disk checking logic
[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) -> Option<BootdiskOptions> {
168 let fstype = self
169 .view
170 .get_child(1)?
171 .downcast_ref::<FormView>()?
172 .get_value::<SelectView<FsType>, _>(0)?;
173
174 let advanced = self.view.get_child_mut(3)?;
175
176 if let Some(view) = advanced.downcast_mut::<LvmBootdiskOptionsView>() {
177 Some(BootdiskOptions {
178 disks: vec![],
179 fstype,
180 advanced: view.get_values().map(AdvancedBootdiskOptions::Lvm)?,
181 })
182 } else if let Some(view) = advanced.downcast_mut::<ZfsBootdiskOptionsView>() {
183 let (disks, advanced) = view.get_values()?;
184
185 Some(BootdiskOptions {
186 disks,
187 fstype,
188 advanced: AdvancedBootdiskOptions::Zfs(advanced),
189 })
190 } else if let Some(view) = advanced.downcast_mut::<BtrfsBootdiskOptionsView>() {
191 let (disks, advanced) = view.get_values()?;
192
193 Some(BootdiskOptions {
194 disks,
195 fstype,
196 advanced: AdvancedBootdiskOptions::Btrfs(advanced),
197 })
198 } else {
199 None
200 }
201 }
202 }
203
204 impl ViewWrapper for AdvancedBootdiskOptionsView {
205 cursive::wrap_impl!(self.view: LinearLayout);
206 }
207
208 struct LvmBootdiskOptionsView {
209 view: FormView,
210 }
211
212 impl LvmBootdiskOptionsView {
213 fn new(options: &LvmBootdiskOptions) -> Self {
214 let is_pve = crate::setup_info().config.product == ProxmoxProduct::PVE;
215 // TODO: Set maximum accordingly to disk size
216 let view = FormView::new()
217 .child(
218 "Total size",
219 DiskSizeEditView::new()
220 .content(options.total_size)
221 .max_value(options.total_size),
222 )
223 .child(
224 "Swap size",
225 DiskSizeEditView::new_emptyable().content_maybe(options.swap_size),
226 )
227 .child_conditional(
228 is_pve,
229 "Maximum root volume size",
230 DiskSizeEditView::new_emptyable().content_maybe(options.max_root_size),
231 )
232 .child_conditional(
233 is_pve,
234 "Maximum data volume size",
235 DiskSizeEditView::new_emptyable().content_maybe(options.max_data_size),
236 )
237 .child(
238 "Minimum free LVM space",
239 DiskSizeEditView::new_emptyable().content_maybe(options.min_lvm_free),
240 );
241
242 Self { view }
243 }
244
245 fn get_values(&mut self) -> Option<LvmBootdiskOptions> {
246 let is_pve = crate::setup_info().config.product == ProxmoxProduct::PVE;
247 let min_lvm_free_id = if is_pve { 4 } else { 2 };
248 let max_root_size = if is_pve {
249 self.view.get_value::<DiskSizeEditView, _>(2)
250 } else {
251 None
252 };
253 let max_data_size = if is_pve {
254 self.view.get_value::<DiskSizeEditView, _>(3)
255 } else {
256 None
257 };
258 Some(LvmBootdiskOptions {
259 total_size: self.view.get_value::<DiskSizeEditView, _>(0)?,
260 swap_size: self.view.get_value::<DiskSizeEditView, _>(1),
261 max_root_size,
262 max_data_size,
263 min_lvm_free: self.view.get_value::<DiskSizeEditView, _>(min_lvm_free_id),
264 })
265 }
266 }
267
268 impl ViewWrapper for LvmBootdiskOptionsView {
269 cursive::wrap_impl!(self.view: FormView);
270 }
271
272 struct MultiDiskOptionsView<T> {
273 view: LinearLayout,
274 phantom: PhantomData<T>,
275 }
276
277 impl<T: View> MultiDiskOptionsView<T> {
278 fn new(avail_disks: &[Disk], selected_disks: &[usize], options_view: T) -> Self {
279 let mut selectable_disks = avail_disks
280 .iter()
281 .map(|d| (d.to_string(), Some(d.clone())))
282 .collect::<Vec<(String, Option<Disk>)>>();
283
284 selectable_disks.push(("-- do not use --".to_owned(), None));
285
286 let mut disk_form = FormView::new();
287 for (i, _) in avail_disks.iter().enumerate() {
288 disk_form.add_child(
289 &format!("Harddisk {i}"),
290 SelectView::new()
291 .popup()
292 .with_all(selectable_disks.clone())
293 .selected(selected_disks[i]),
294 );
295 }
296
297 let mut disk_select_view = LinearLayout::vertical()
298 .child(TextView::new("Disk setup").center())
299 .child(DummyView)
300 .child(ScrollView::new(disk_form.with_name("multidisk-disk-form")));
301
302 if avail_disks.len() > 3 {
303 let do_not_use_index = selectable_disks.len() - 1;
304 let deselect_all_button = Button::new("Deselect all", move |siv| {
305 siv.call_on_name("multidisk-disk-form", |view: &mut FormView| {
306 view.call_on_childs(&|v: &mut SelectView<Option<Disk>>| {
307 // As there is no .on_select() callback defined on the
308 // SelectView's, the returned callback here can be safely
309 // ignored.
310 v.set_selection(do_not_use_index);
311 });
312 });
313 });
314
315 disk_select_view.add_child(PaddedView::lrtb(
316 0,
317 0,
318 1,
319 0,
320 LinearLayout::horizontal()
321 .child(DummyView.full_width())
322 .child(deselect_all_button),
323 ));
324 }
325
326 let options_view = LinearLayout::vertical()
327 .child(TextView::new("Advanced options").center())
328 .child(DummyView)
329 .child(options_view);
330
331 let view = LinearLayout::horizontal()
332 .child(disk_select_view)
333 .child(DummyView.fixed_width(3))
334 .child(options_view);
335
336 Self {
337 view: LinearLayout::vertical().child(view),
338 phantom: PhantomData,
339 }
340 }
341
342 fn top_panel(mut self, view: impl View) -> Self {
343 if self.has_top_panel() {
344 self.view.remove_child(0);
345 }
346
347 self.view.insert_child(0, Panel::new(view));
348 self
349 }
350
351 ///
352 /// This function returns a tuple of vectors. The first vector contains the currently selected
353 /// disks in order of their selection slot. Empty slots are filtered out. The second vector
354 /// contains indices of each slot's selection, which enables us to restore the selection even
355 /// for empty slots.
356 ///
357 fn get_disks_and_selection(&mut self) -> Option<(Vec<Disk>, Vec<usize>)> {
358 let mut disks = vec![];
359 let view_top_index = usize::from(self.has_top_panel());
360
361 let disk_form = self
362 .view
363 .get_child_mut(view_top_index)?
364 .downcast_mut::<LinearLayout>()?
365 .get_child_mut(0)?
366 .downcast_mut::<LinearLayout>()?
367 .get_child_mut(2)?
368 .downcast_mut::<ScrollView<NamedView<FormView>>>()?
369 .get_inner_mut()
370 .get_mut();
371
372 let mut selected_disks = Vec::new();
373
374 for i in 0..disk_form.len() {
375 let disk = disk_form.get_value::<SelectView<Option<Disk>>, _>(i)?;
376
377 // `None` means no disk was selected for this slot
378 if let Some(disk) = disk {
379 disks.push(disk);
380 }
381
382 selected_disks.push(
383 disk_form
384 .get_child::<SelectView<Option<Disk>>>(i)?
385 .selected_id()?,
386 );
387 }
388
389 Some((disks, selected_disks))
390 }
391
392 fn inner_mut(&mut self) -> Option<&mut T> {
393 let view_top_index = usize::from(self.has_top_panel());
394
395 self.view
396 .get_child_mut(view_top_index)?
397 .downcast_mut::<LinearLayout>()?
398 .get_child_mut(2)?
399 .downcast_mut::<LinearLayout>()?
400 .get_child_mut(2)?
401 .downcast_mut::<T>()
402 }
403
404 fn has_top_panel(&self) -> bool {
405 // The root view should only ever have one or two children
406 assert!([1, 2].contains(&self.view.len()));
407
408 self.view.len() == 2
409 }
410 }
411
412 impl<T: 'static> ViewWrapper for MultiDiskOptionsView<T> {
413 cursive::wrap_impl!(self.view: LinearLayout);
414 }
415
416 struct BtrfsBootdiskOptionsView {
417 view: MultiDiskOptionsView<FormView>,
418 }
419
420 impl BtrfsBootdiskOptionsView {
421 fn new(disks: &[Disk], options: &BtrfsBootdiskOptions) -> Self {
422 let view = MultiDiskOptionsView::new(
423 disks,
424 &options.selected_disks,
425 FormView::new().child("hdsize", DiskSizeEditView::new().content(options.disk_size)),
426 )
427 .top_panel(TextView::new("Btrfs integration is a technology preview!").center());
428
429 Self { view }
430 }
431
432 fn get_values(&mut self) -> Option<(Vec<Disk>, BtrfsBootdiskOptions)> {
433 let (disks, selected_disks) = self.view.get_disks_and_selection()?;
434 let disk_size = self.view.inner_mut()?.get_value::<DiskSizeEditView, _>(0)?;
435
436 Some((
437 disks,
438 BtrfsBootdiskOptions {
439 disk_size,
440 selected_disks,
441 },
442 ))
443 }
444 }
445
446 impl ViewWrapper for BtrfsBootdiskOptionsView {
447 cursive::wrap_impl!(self.view: MultiDiskOptionsView<FormView>);
448 }
449
450 struct ZfsBootdiskOptionsView {
451 view: MultiDiskOptionsView<FormView>,
452 }
453
454 impl ZfsBootdiskOptionsView {
455 // TODO: Re-apply previous disk selection from `options` correctly
456 fn new(disks: &[Disk], options: &ZfsBootdiskOptions) -> Self {
457 let inner = FormView::new()
458 .child("ashift", IntegerEditView::new().content(options.ashift))
459 .child(
460 "compress",
461 SelectView::new()
462 .popup()
463 .with_all(ZFS_COMPRESS_OPTIONS.iter().map(|o| (o.to_string(), *o)))
464 .selected(
465 ZFS_COMPRESS_OPTIONS
466 .iter()
467 .position(|o| *o == options.compress)
468 .unwrap_or_default(),
469 ),
470 )
471 .child(
472 "checksum",
473 SelectView::new()
474 .popup()
475 .with_all(ZFS_CHECKSUM_OPTIONS.iter().map(|o| (o.to_string(), *o)))
476 .selected(
477 ZFS_CHECKSUM_OPTIONS
478 .iter()
479 .position(|o| *o == options.checksum)
480 .unwrap_or_default(),
481 ),
482 )
483 .child("copies", IntegerEditView::new().content(options.copies))
484 .child("hdsize", DiskSizeEditView::new().content(options.disk_size));
485
486 let view = MultiDiskOptionsView::new(disks, &options.selected_disks, inner)
487 .top_panel(TextView::new(
488 "ZFS is not compatible with hardware RAID controllers, for details see the documentation."
489 ).center());
490
491 Self { view }
492 }
493
494 fn get_values(&mut self) -> Option<(Vec<Disk>, ZfsBootdiskOptions)> {
495 let (disks, selected_disks) = self.view.get_disks_and_selection()?;
496 let view = self.view.inner_mut()?;
497
498 let ashift = view.get_value::<IntegerEditView, _>(0)?;
499 let compress = view.get_value::<SelectView<_>, _>(1)?;
500 let checksum = view.get_value::<SelectView<_>, _>(2)?;
501 let copies = view.get_value::<IntegerEditView, _>(3)?;
502 let disk_size = view.get_value::<DiskSizeEditView, _>(4)?;
503
504 Some((
505 disks,
506 ZfsBootdiskOptions {
507 ashift,
508 compress,
509 checksum,
510 copies,
511 disk_size,
512 selected_disks,
513 },
514 ))
515 }
516 }
517
518 impl ViewWrapper for ZfsBootdiskOptionsView {
519 cursive::wrap_impl!(self.view: MultiDiskOptionsView<FormView>);
520 }
521
522 fn advanced_options_view(disks: &[Disk], options: Rc<RefCell<BootdiskOptions>>) -> impl View {
523 Dialog::around(AdvancedBootdiskOptionsView::new(
524 disks,
525 &(*options).borrow(),
526 ))
527 .title("Advanced bootdisk options")
528 .button("Ok", {
529 let options_ref = options.clone();
530 move |siv| {
531 let options = siv
532 .call_on_name("advanced-bootdisk-options-dialog", |view: &mut Dialog| {
533 view.get_content_mut()
534 .downcast_mut()
535 .and_then(AdvancedBootdiskOptionsView::get_values)
536 })
537 .flatten();
538
539 if let Err(duplicate) = check_for_duplicate_disks(&options.disks) {
540 siv.add_layer(Dialog::info(format!(
541 "Cannot select same disk twice: {duplicate}"
542 )));
543 return;
544 }
545
546 siv.pop_layer();
547 if let Some(options) = options {
548 *(*options_ref).borrow_mut() = options;
549 }
550 }
551 })
552 .with_name("advanced-bootdisk-options-dialog")
553 .max_size((120, 40))
554 }
555
556 /// Checks a list of disks for duplicate entries, using their index as key.
557 ///
558 /// # Arguments
559 ///
560 /// * `disks` - A list of disks to check for duplicates.
561 fn check_for_duplicate_disks(disks: &[Disk]) -> Result<(), &Disk> {
562 let mut set = HashSet::new();
563
564 for disk in disks {
565 if !set.insert(&disk.index) {
566 return Err(disk);
567 }
568 }
569
570 Ok(())
571 }