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