]> git.proxmox.com Git - pve-installer.git/blob - proxmox-tui-installer/src/views/bootdisk.rs
3b2242a6ff4ba9691520a6e8598354c8710b34d4
[pve-installer.git] / proxmox-tui-installer / src / views / bootdisk.rs
1 use std::{cell::RefCell, 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::FS_TYPES;
14 use crate::InstallerState;
15
16 use proxmox_installer_common::{
17 disk_checks::{
18 check_btrfs_raid_config, check_disks_4kn_legacy_boot, check_for_duplicate_disks,
19 check_zfs_raid_config,
20 },
21 options::{
22 AdvancedBootdiskOptions, BootdiskOptions, BtrfsBootdiskOptions, Disk, FsType,
23 LvmBootdiskOptions, ZfsBootdiskOptions, ZFS_CHECKSUM_OPTIONS, ZFS_COMPRESS_OPTIONS,
24 },
25 setup::{BootType, ProductConfig, ProxmoxProduct, RuntimeInfo},
26 };
27
28 pub struct BootdiskOptionsView {
29 view: LinearLayout,
30 advanced_options: Rc<RefCell<BootdiskOptions>>,
31 boot_type: BootType,
32 }
33
34 impl BootdiskOptionsView {
35 pub fn new(siv: &mut Cursive, disks: &[Disk], options: &BootdiskOptions) -> Self {
36 let bootdisk_form = FormView::new()
37 .child(
38 "Target harddisk",
39 SelectView::new()
40 .popup()
41 .with_all(disks.iter().map(|d| (d.to_string(), d.clone()))),
42 )
43 .with_name("bootdisk-options-target-disk");
44
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
49
50 let advanced_options = Rc::new(RefCell::new(options.clone()));
51
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();
57 move |siv| {
58 siv.add_layer(advanced_options_view(
59 &disks,
60 options.clone(),
61 product_conf.clone(),
62 ));
63 }
64 }));
65
66 let view = LinearLayout::vertical()
67 .child(bootdisk_form)
68 .child(DummyView)
69 .child(advanced_button);
70
71 let boot_type = siv
72 .user_data::<InstallerState>()
73 .map(|state| state.runtime_info.boot_type)
74 .unwrap_or(BootType::Bios);
75
76 Self {
77 view,
78 advanced_options,
79 boot_type,
80 }
81 }
82
83 pub fn get_values(&mut self) -> Result<BootdiskOptions, String> {
84 let mut options = (*self.advanced_options).clone().into_inner();
85
86 if [FsType::Ext4, FsType::Xfs].contains(&options.fstype) {
87 let disk = self
88 .view
89 .get_child_mut(0)
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")?;
94
95 options.disks = vec![disk];
96 }
97
98 check_disks_4kn_legacy_boot(self.boot_type, &options.disks)?;
99 Ok(options)
100 }
101 }
102
103 impl ViewWrapper for BootdiskOptionsView {
104 cursive::wrap_impl!(self.view: LinearLayout);
105 }
106
107 struct AdvancedBootdiskOptionsView {
108 view: LinearLayout,
109 }
110
111 impl AdvancedBootdiskOptionsView {
112 fn new(disks: &[Disk], options: &BootdiskOptions, product_conf: ProductConfig) -> Self {
113 let filter_btrfs =
114 |fstype: &&FsType| -> bool { product_conf.enable_btrfs || !fstype.is_btrfs() };
115
116 let fstype_select = SelectView::new()
117 .popup()
118 .with_all(
119 FS_TYPES
120 .iter()
121 .filter(filter_btrfs)
122 .map(|t| (t.to_string(), *t)),
123 )
124 .selected(
125 FS_TYPES
126 .iter()
127 .filter(filter_btrfs)
128 .position(|t| *t == options.fstype)
129 .unwrap_or_default(),
130 )
131 .on_submit({
132 let disks = disks.to_owned();
133 move |siv, fstype| Self::fstype_on_submit(siv, &disks, fstype)
134 });
135
136 let mut view = LinearLayout::vertical()
137 .child(DummyView.full_width())
138 .child(FormView::new().child("Filesystem", fstype_select))
139 .child(DummyView.full_width());
140
141 match &options.advanced {
142 AdvancedBootdiskOptions::Lvm(lvm) => {
143 view.add_child(LvmBootdiskOptionsView::new(lvm, &product_conf))
144 }
145 AdvancedBootdiskOptions::Zfs(zfs) => {
146 view.add_child(ZfsBootdiskOptionsView::new(disks, zfs))
147 }
148 AdvancedBootdiskOptions::Btrfs(btrfs) => {
149 view.add_child(BtrfsBootdiskOptionsView::new(disks, btrfs))
150 }
151 };
152
153 Self { view }
154 }
155
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();
160
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()
164 {
165 view.remove_child(3);
166 match fstype {
167 FsType::Ext4 | FsType::Xfs => view.add_child(
168 LvmBootdiskOptionsView::new_with_defaults(&disks[0], &product_conf),
169 ),
170 FsType::Zfs(_) => view.add_child(ZfsBootdiskOptionsView::new_with_defaults(
171 &runinfo,
172 &product_conf,
173 )),
174 FsType::Btrfs(_) => {
175 view.add_child(BtrfsBootdiskOptionsView::new_with_defaults(disks))
176 }
177 }
178 }
179 });
180
181 siv.call_on_name(
182 "bootdisk-options-target-disk",
183 |view: &mut FormView| match fstype {
184 FsType::Ext4 | FsType::Xfs => {
185 view.replace_child(
186 0,
187 SelectView::new()
188 .popup()
189 .with_all(disks.iter().map(|d| (d.to_string(), d.clone()))),
190 );
191 }
192 other => view.replace_child(0, TextView::new(other.to_string())),
193 },
194 );
195 }
196
197 fn get_values(&mut self) -> Result<BootdiskOptions, String> {
198 let fstype = self
199 .view
200 .get_child(1)
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())?;
204
205 let advanced = self
206 .view
207 .get_child_mut(3)
208 .ok_or("Failed to retrieve advanced bootdisk options view".to_owned())?;
209
210 if let Some(view) = advanced.downcast_mut::<LvmBootdiskOptionsView>() {
211 let advanced = view
212 .get_values()
213 .map(AdvancedBootdiskOptions::Lvm)
214 .ok_or("Failed to retrieve advanced bootdisk options")?;
215
216 Ok(BootdiskOptions {
217 disks: vec![],
218 fstype,
219 advanced,
220 })
221 } else if let Some(view) = advanced.downcast_mut::<ZfsBootdiskOptionsView>() {
222 let (disks, advanced) = view
223 .get_values()
224 .ok_or("Failed to retrieve advanced bootdisk options")?;
225
226 if let FsType::Zfs(level) = fstype {
227 check_zfs_raid_config(level, &disks).map_err(|err| format!("{fstype}: {err}"))?;
228 }
229
230 Ok(BootdiskOptions {
231 disks,
232 fstype,
233 advanced: AdvancedBootdiskOptions::Zfs(advanced),
234 })
235 } else if let Some(view) = advanced.downcast_mut::<BtrfsBootdiskOptionsView>() {
236 let (disks, advanced) = view
237 .get_values()
238 .ok_or("Failed to retrieve advanced bootdisk options")?;
239
240 if let FsType::Btrfs(level) = fstype {
241 check_btrfs_raid_config(level, &disks).map_err(|err| format!("{fstype}: {err}"))?;
242 }
243
244 Ok(BootdiskOptions {
245 disks,
246 fstype,
247 advanced: AdvancedBootdiskOptions::Btrfs(advanced),
248 })
249 } else {
250 Err("Invalid bootdisk view state".to_owned())
251 }
252 }
253 }
254
255 impl ViewWrapper for AdvancedBootdiskOptionsView {
256 cursive::wrap_impl!(self.view: LinearLayout);
257 }
258
259 struct LvmBootdiskOptionsView {
260 view: FormView,
261 has_extra_fields: bool,
262 }
263
264 impl LvmBootdiskOptionsView {
265 fn new(options: &LvmBootdiskOptions, product_conf: &ProductConfig) -> Self {
266 let show_extra_fields = product_conf.product == ProxmoxProduct::PVE;
267
268 // TODO: Set maximum accordingly to disk size
269 let view = FormView::new()
270 .child(
271 "Total size",
272 DiskSizeEditView::new()
273 .content(options.total_size)
274 .max_value(options.total_size),
275 )
276 .child(
277 "Swap size",
278 DiskSizeEditView::new_emptyable().content_maybe(options.swap_size),
279 )
280 .child_conditional(
281 show_extra_fields,
282 "Maximum root volume size",
283 DiskSizeEditView::new_emptyable().content_maybe(options.max_root_size),
284 )
285 .child_conditional(
286 show_extra_fields,
287 "Maximum data volume size",
288 DiskSizeEditView::new_emptyable().content_maybe(options.max_data_size),
289 )
290 .child(
291 "Minimum free LVM space",
292 DiskSizeEditView::new_emptyable().content_maybe(options.min_lvm_free),
293 );
294
295 Self {
296 view,
297 has_extra_fields: show_extra_fields,
298 }
299 }
300
301 fn new_with_defaults(disk: &Disk, product_conf: &ProductConfig) -> Self {
302 Self::new(&LvmBootdiskOptions::defaults_from(disk), product_conf)
303 }
304
305 fn get_values(&mut self) -> Option<LvmBootdiskOptions> {
306 let min_lvm_free_id = if self.has_extra_fields { 4 } else { 2 };
307
308 let max_root_size = self
309 .has_extra_fields
310 .then(|| self.view.get_value::<DiskSizeEditView, _>(2))
311 .flatten();
312 let max_data_size = self
313 .has_extra_fields
314 .then(|| self.view.get_value::<DiskSizeEditView, _>(3))
315 .flatten();
316
317 Some(LvmBootdiskOptions {
318 total_size: self.view.get_value::<DiskSizeEditView, _>(0)?,
319 swap_size: self.view.get_value::<DiskSizeEditView, _>(1),
320 max_root_size,
321 max_data_size,
322 min_lvm_free: self.view.get_value::<DiskSizeEditView, _>(min_lvm_free_id),
323 })
324 }
325 }
326
327 impl ViewWrapper for LvmBootdiskOptionsView {
328 cursive::wrap_impl!(self.view: FormView);
329 }
330
331 struct MultiDiskOptionsView<T> {
332 view: LinearLayout,
333 phantom: PhantomData<T>,
334 }
335
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
339 .iter()
340 .map(|d| (d.to_string(), Some(d.clone())))
341 .collect::<Vec<(String, Option<Disk>)>>();
342
343 selectable_disks.push(("-- do not use --".to_owned(), None));
344
345 let mut disk_form = FormView::new();
346 for (i, _) in avail_disks.iter().enumerate() {
347 disk_form.add_child(
348 &format!("Harddisk {i}"),
349 SelectView::new()
350 .popup()
351 .with_all(selectable_disks.clone())
352 .selected(selected_disks[i]),
353 );
354 }
355
356 let mut disk_select_view = LinearLayout::vertical()
357 .child(TextView::new("Disk setup").center())
358 .child(DummyView)
359 .child(ScrollView::new(disk_form.with_name("multidisk-disk-form")));
360
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
368 // ignored.
369 v.set_selection(do_not_use_index);
370 });
371 });
372 });
373
374 disk_select_view.add_child(PaddedView::lrtb(
375 0,
376 0,
377 1,
378 0,
379 LinearLayout::horizontal()
380 .child(DummyView.full_width())
381 .child(deselect_all_button),
382 ));
383 }
384
385 let options_view = LinearLayout::vertical()
386 .child(TextView::new("Advanced options").center())
387 .child(DummyView)
388 .child(options_view);
389
390 let view = LinearLayout::horizontal()
391 .child(disk_select_view)
392 .child(DummyView.fixed_width(3))
393 .child(options_view);
394
395 Self {
396 view: LinearLayout::vertical().child(view),
397 phantom: PhantomData,
398 }
399 }
400
401 fn top_panel(mut self, view: impl View) -> Self {
402 if self.has_top_panel() {
403 self.view.remove_child(0);
404 }
405
406 self.view.insert_child(0, Panel::new(view));
407 self
408 }
409
410 ///
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
414 /// for empty slots.
415 ///
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());
419
420 let disk_form = self
421 .view
422 .get_child_mut(view_top_index)?
423 .downcast_mut::<LinearLayout>()?
424 .get_child_mut(0)?
425 .downcast_mut::<LinearLayout>()?
426 .get_child_mut(2)?
427 .downcast_mut::<ScrollView<NamedView<FormView>>>()?
428 .get_inner_mut()
429 .get_mut();
430
431 let mut selected_disks = Vec::new();
432
433 for i in 0..disk_form.len() {
434 let disk = disk_form.get_value::<SelectView<Option<Disk>>, _>(i)?;
435
436 // `None` means no disk was selected for this slot
437 if let Some(disk) = disk {
438 disks.push(disk);
439 }
440
441 selected_disks.push(
442 disk_form
443 .get_child::<SelectView<Option<Disk>>>(i)?
444 .selected_id()?,
445 );
446 }
447
448 Some((disks, selected_disks))
449 }
450
451 fn inner_mut(&mut self) -> Option<&mut T> {
452 let view_top_index = usize::from(self.has_top_panel());
453
454 self.view
455 .get_child_mut(view_top_index)?
456 .downcast_mut::<LinearLayout>()?
457 .get_child_mut(2)?
458 .downcast_mut::<LinearLayout>()?
459 .get_child_mut(2)?
460 .downcast_mut::<T>()
461 }
462
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()));
466
467 self.view.len() == 2
468 }
469 }
470
471 impl<T: 'static> ViewWrapper for MultiDiskOptionsView<T> {
472 cursive::wrap_impl!(self.view: LinearLayout);
473 }
474
475 struct BtrfsBootdiskOptionsView {
476 view: MultiDiskOptionsView<FormView>,
477 }
478
479 impl BtrfsBootdiskOptionsView {
480 fn new(disks: &[Disk], options: &BtrfsBootdiskOptions) -> Self {
481 let view = MultiDiskOptionsView::new(
482 disks,
483 &options.selected_disks,
484 FormView::new().child("hdsize", DiskSizeEditView::new().content(options.disk_size)),
485 )
486 .top_panel(TextView::new("Btrfs integration is a technology preview!").center());
487
488 Self { view }
489 }
490
491 fn new_with_defaults(disks: &[Disk]) -> Self {
492 Self::new(disks, &BtrfsBootdiskOptions::defaults_from(disks))
493 }
494
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)?;
498
499 Some((
500 disks,
501 BtrfsBootdiskOptions {
502 disk_size,
503 selected_disks,
504 },
505 ))
506 }
507 }
508
509 impl ViewWrapper for BtrfsBootdiskOptionsView {
510 cursive::wrap_impl!(self.view: MultiDiskOptionsView<FormView>);
511 }
512
513 struct ZfsBootdiskOptionsView {
514 view: MultiDiskOptionsView<FormView>,
515 }
516
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))
522 .child(
523 "compress",
524 SelectView::new()
525 .popup()
526 .with_all(ZFS_COMPRESS_OPTIONS.iter().map(|o| (o.to_string(), *o)))
527 .selected(
528 ZFS_COMPRESS_OPTIONS
529 .iter()
530 .position(|o| *o == options.compress)
531 .unwrap_or_default(),
532 ),
533 )
534 .child(
535 "checksum",
536 SelectView::new()
537 .popup()
538 .with_all(ZFS_CHECKSUM_OPTIONS.iter().map(|o| (o.to_string(), *o)))
539 .selected(
540 ZFS_CHECKSUM_OPTIONS
541 .iter()
542 .position(|o| *o == options.checksum)
543 .unwrap_or_default(),
544 ),
545 )
546 .child("copies", IntegerEditView::new().content(options.copies))
547 .child("hdsize", DiskSizeEditView::new().content(options.disk_size));
548
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."
552 ).center());
553
554 Self { view }
555 }
556
557 fn new_with_defaults(runinfo: &RuntimeInfo, product_conf: &ProductConfig) -> Self {
558 Self::new(
559 &runinfo.disks,
560 &ZfsBootdiskOptions::defaults_from(runinfo, product_conf),
561 )
562 }
563
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()?;
567
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)?;
573
574 Some((
575 disks,
576 ZfsBootdiskOptions {
577 ashift,
578 compress,
579 checksum,
580 copies,
581 arc_max: 0, // use built-in ZFS default value
582 disk_size,
583 selected_disks,
584 },
585 ))
586 }
587 }
588
589 impl ViewWrapper for ZfsBootdiskOptionsView {
590 cursive::wrap_impl!(self.view: MultiDiskOptionsView<FormView>);
591 }
592
593 fn advanced_options_view(
594 disks: &[Disk],
595 options: Rc<RefCell<BootdiskOptions>>,
596 product_conf: ProductConfig,
597 ) -> impl View {
598 Dialog::around(AdvancedBootdiskOptionsView::new(
599 disks,
600 &(*options).borrow(),
601 product_conf,
602 ))
603 .title("Advanced bootdisk options")
604 .button("Ok", {
605 let options_ref = options.clone();
606 move |siv| {
607 let options = siv
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)
612 })
613 .flatten();
614
615 let options = match options {
616 Some(Ok(options)) => options,
617 Some(Err(err)) => {
618 siv.add_layer(Dialog::info(err));
619 return;
620 }
621 None => {
622 siv.add_layer(Dialog::info("Failed to retrieve bootdisk options view"));
623 return;
624 }
625 };
626
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}"
630 )));
631 return;
632 }
633
634 siv.pop_layer();
635 *(*options_ref).borrow_mut() = options;
636 }
637 })
638 .with_name("advanced-bootdisk-options-dialog")
639 .max_size((120, 40))
640 }