]> git.proxmox.com Git - pve-installer.git/blob - proxmox-tui-installer/src/views/bootdisk.rs
tui: setup: handle missing disk block size gracefully
[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::{
14 options::{
15 AdvancedBootdiskOptions, BootdiskOptions, BtrfsBootdiskOptions, BtrfsRaidLevel, Disk,
16 FsType, LvmBootdiskOptions, ZfsBootdiskOptions, ZfsRaidLevel, FS_TYPES,
17 ZFS_CHECKSUM_OPTIONS, ZFS_COMPRESS_OPTIONS,
18 },
19 setup::{BootType, RuntimeInfo},
20 };
21 use crate::{setup::ProxmoxProduct, InstallerState};
22
23 pub struct BootdiskOptionsView {
24 view: LinearLayout,
25 advanced_options: Rc<RefCell<BootdiskOptions>>,
26 }
27
28 impl BootdiskOptionsView {
29 pub fn new(disks: &[Disk], options: &BootdiskOptions) -> Self {
30 let bootdisk_form = FormView::new()
31 .child(
32 "Target harddisk",
33 SelectView::new()
34 .popup()
35 .with_all(disks.iter().map(|d| (d.to_string(), d.clone()))),
36 )
37 .with_name("bootdisk-options-target-disk");
38
39 let advanced_options = Rc::new(RefCell::new(options.clone()));
40
41 let advanced_button = LinearLayout::horizontal()
42 .child(DummyView.full_width())
43 .child(Button::new("Advanced options", {
44 let disks = disks.to_owned();
45 let options = advanced_options.clone();
46 move |siv| {
47 siv.add_layer(advanced_options_view(&disks, options.clone()));
48 }
49 }));
50
51 let view = LinearLayout::vertical()
52 .child(bootdisk_form)
53 .child(DummyView)
54 .child(advanced_button);
55
56 Self {
57 view,
58 advanced_options,
59 }
60 }
61
62 pub fn get_values(&mut self) -> Result<BootdiskOptions, String> {
63 let mut options = (*self.advanced_options).clone().into_inner();
64
65 if [FsType::Ext4, FsType::Xfs].contains(&options.fstype) {
66 let disk = self
67 .view
68 .get_child_mut(0)
69 .and_then(|v| v.downcast_mut::<NamedView<FormView>>())
70 .map(NamedView::<FormView>::get_mut)
71 .and_then(|v| v.get_value::<SelectView<Disk>, _>(0))
72 .ok_or("failed to retrieve bootdisk")?;
73
74 options.disks = vec![disk];
75 }
76
77 Ok(options)
78 }
79 }
80
81 impl ViewWrapper for BootdiskOptionsView {
82 cursive::wrap_impl!(self.view: LinearLayout);
83 }
84
85 struct AdvancedBootdiskOptionsView {
86 view: LinearLayout,
87 }
88
89 impl AdvancedBootdiskOptionsView {
90 fn new(disks: &[Disk], options: &BootdiskOptions) -> Self {
91 let enable_btrfs = crate::setup_info().config.enable_btrfs;
92
93 let filter_btrfs = |fstype: &&FsType| -> bool { enable_btrfs || !fstype.is_btrfs() };
94
95 let fstype_select = SelectView::new()
96 .popup()
97 .with_all(
98 FS_TYPES
99 .iter()
100 .filter(filter_btrfs)
101 .map(|t| (t.to_string(), *t)),
102 )
103 .selected(
104 FS_TYPES
105 .iter()
106 .filter(filter_btrfs)
107 .position(|t| *t == options.fstype)
108 .unwrap_or_default(),
109 )
110 .on_submit({
111 let disks = disks.to_owned();
112 move |siv, fstype| Self::fstype_on_submit(siv, &disks, fstype)
113 });
114
115 let mut view = LinearLayout::vertical()
116 .child(DummyView.full_width())
117 .child(FormView::new().child("Filesystem", fstype_select))
118 .child(DummyView.full_width());
119
120 match &options.advanced {
121 AdvancedBootdiskOptions::Lvm(lvm) => view.add_child(LvmBootdiskOptionsView::new(lvm)),
122 AdvancedBootdiskOptions::Zfs(zfs) => {
123 view.add_child(ZfsBootdiskOptionsView::new(disks, zfs))
124 }
125 AdvancedBootdiskOptions::Btrfs(btrfs) => {
126 view.add_child(BtrfsBootdiskOptionsView::new(disks, btrfs))
127 }
128 };
129
130 Self { view }
131 }
132
133 fn fstype_on_submit(siv: &mut Cursive, disks: &[Disk], fstype: &FsType) {
134 siv.call_on_name("advanced-bootdisk-options-dialog", |view: &mut Dialog| {
135 if let Some(AdvancedBootdiskOptionsView { view }) =
136 view.get_content_mut().downcast_mut()
137 {
138 view.remove_child(3);
139 match fstype {
140 FsType::Ext4 | FsType::Xfs => view.add_child(LvmBootdiskOptionsView::new(
141 &LvmBootdiskOptions::defaults_from(&disks[0]),
142 )),
143 FsType::Zfs(_) => view.add_child(ZfsBootdiskOptionsView::new(
144 disks,
145 &ZfsBootdiskOptions::defaults_from(disks),
146 )),
147 FsType::Btrfs(_) => view.add_child(BtrfsBootdiskOptionsView::new(
148 disks,
149 &BtrfsBootdiskOptions::defaults_from(disks),
150 )),
151 }
152 }
153 });
154
155 siv.call_on_name(
156 "bootdisk-options-target-disk",
157 |view: &mut FormView| match fstype {
158 FsType::Ext4 | FsType::Xfs => {
159 view.replace_child(
160 0,
161 SelectView::new()
162 .popup()
163 .with_all(disks.iter().map(|d| (d.to_string(), d.clone()))),
164 );
165 }
166 other => view.replace_child(0, TextView::new(other.to_string())),
167 },
168 );
169 }
170
171 fn get_values(&mut self, runinfo: &RuntimeInfo) -> Result<BootdiskOptions, String> {
172 let fstype = self
173 .view
174 .get_child(1)
175 .and_then(|v| v.downcast_ref::<FormView>())
176 .and_then(|v| v.get_value::<SelectView<FsType>, _>(0))
177 .ok_or("Failed to retrieve filesystem type".to_owned())?;
178
179 let advanced = self
180 .view
181 .get_child_mut(3)
182 .ok_or("Failed to retrieve advanced bootdisk options view".to_owned())?;
183
184 if let Some(view) = advanced.downcast_mut::<LvmBootdiskOptionsView>() {
185 let advanced = view
186 .get_values()
187 .map(AdvancedBootdiskOptions::Lvm)
188 .ok_or("Failed to retrieve advanced bootdisk options")?;
189
190 Ok(BootdiskOptions {
191 disks: vec![],
192 fstype,
193 advanced,
194 })
195 } else if let Some(view) = advanced.downcast_mut::<ZfsBootdiskOptionsView>() {
196 let (disks, advanced) = view
197 .get_values()
198 .ok_or("Failed to retrieve advanced bootdisk options")?;
199
200 if let FsType::Zfs(level) = fstype {
201 check_zfs_raid_config(runinfo, level, &disks)
202 .map_err(|err| format!("{fstype}: {err}"))?;
203 }
204
205 Ok(BootdiskOptions {
206 disks,
207 fstype,
208 advanced: AdvancedBootdiskOptions::Zfs(advanced),
209 })
210 } else if let Some(view) = advanced.downcast_mut::<BtrfsBootdiskOptionsView>() {
211 let (disks, advanced) = view
212 .get_values()
213 .ok_or("Failed to retrieve advanced bootdisk options")?;
214
215 if let FsType::Btrfs(level) = fstype {
216 check_btrfs_raid_config(level, &disks).map_err(|err| format!("{fstype}: {err}"))?;
217 }
218
219 Ok(BootdiskOptions {
220 disks,
221 fstype,
222 advanced: AdvancedBootdiskOptions::Btrfs(advanced),
223 })
224 } else {
225 Err("Invalid bootdisk view state".to_owned())
226 }
227 }
228 }
229
230 impl ViewWrapper for AdvancedBootdiskOptionsView {
231 cursive::wrap_impl!(self.view: LinearLayout);
232 }
233
234 struct LvmBootdiskOptionsView {
235 view: FormView,
236 }
237
238 impl LvmBootdiskOptionsView {
239 fn new(options: &LvmBootdiskOptions) -> Self {
240 let is_pve = crate::setup_info().config.product == ProxmoxProduct::PVE;
241 // TODO: Set maximum accordingly to disk size
242 let view = FormView::new()
243 .child(
244 "Total size",
245 DiskSizeEditView::new()
246 .content(options.total_size)
247 .max_value(options.total_size),
248 )
249 .child(
250 "Swap size",
251 DiskSizeEditView::new_emptyable().content_maybe(options.swap_size),
252 )
253 .child_conditional(
254 is_pve,
255 "Maximum root volume size",
256 DiskSizeEditView::new_emptyable().content_maybe(options.max_root_size),
257 )
258 .child_conditional(
259 is_pve,
260 "Maximum data volume size",
261 DiskSizeEditView::new_emptyable().content_maybe(options.max_data_size),
262 )
263 .child(
264 "Minimum free LVM space",
265 DiskSizeEditView::new_emptyable().content_maybe(options.min_lvm_free),
266 );
267
268 Self { view }
269 }
270
271 fn get_values(&mut self) -> Option<LvmBootdiskOptions> {
272 let is_pve = crate::setup_info().config.product == ProxmoxProduct::PVE;
273 let min_lvm_free_id = if is_pve { 4 } else { 2 };
274 let max_root_size = if is_pve {
275 self.view.get_value::<DiskSizeEditView, _>(2)
276 } else {
277 None
278 };
279 let max_data_size = if is_pve {
280 self.view.get_value::<DiskSizeEditView, _>(3)
281 } else {
282 None
283 };
284 Some(LvmBootdiskOptions {
285 total_size: self.view.get_value::<DiskSizeEditView, _>(0)?,
286 swap_size: self.view.get_value::<DiskSizeEditView, _>(1),
287 max_root_size,
288 max_data_size,
289 min_lvm_free: self.view.get_value::<DiskSizeEditView, _>(min_lvm_free_id),
290 })
291 }
292 }
293
294 impl ViewWrapper for LvmBootdiskOptionsView {
295 cursive::wrap_impl!(self.view: FormView);
296 }
297
298 struct MultiDiskOptionsView<T> {
299 view: LinearLayout,
300 phantom: PhantomData<T>,
301 }
302
303 impl<T: View> MultiDiskOptionsView<T> {
304 fn new(avail_disks: &[Disk], selected_disks: &[usize], options_view: T) -> Self {
305 let mut selectable_disks = avail_disks
306 .iter()
307 .map(|d| (d.to_string(), Some(d.clone())))
308 .collect::<Vec<(String, Option<Disk>)>>();
309
310 selectable_disks.push(("-- do not use --".to_owned(), None));
311
312 let mut disk_form = FormView::new();
313 for (i, _) in avail_disks.iter().enumerate() {
314 disk_form.add_child(
315 &format!("Harddisk {i}"),
316 SelectView::new()
317 .popup()
318 .with_all(selectable_disks.clone())
319 .selected(selected_disks[i]),
320 );
321 }
322
323 let mut disk_select_view = LinearLayout::vertical()
324 .child(TextView::new("Disk setup").center())
325 .child(DummyView)
326 .child(ScrollView::new(disk_form.with_name("multidisk-disk-form")));
327
328 if avail_disks.len() > 3 {
329 let do_not_use_index = selectable_disks.len() - 1;
330 let deselect_all_button = Button::new("Deselect all", move |siv| {
331 siv.call_on_name("multidisk-disk-form", |view: &mut FormView| {
332 view.call_on_childs(&|v: &mut SelectView<Option<Disk>>| {
333 // As there is no .on_select() callback defined on the
334 // SelectView's, the returned callback here can be safely
335 // ignored.
336 v.set_selection(do_not_use_index);
337 });
338 });
339 });
340
341 disk_select_view.add_child(PaddedView::lrtb(
342 0,
343 0,
344 1,
345 0,
346 LinearLayout::horizontal()
347 .child(DummyView.full_width())
348 .child(deselect_all_button),
349 ));
350 }
351
352 let options_view = LinearLayout::vertical()
353 .child(TextView::new("Advanced options").center())
354 .child(DummyView)
355 .child(options_view);
356
357 let view = LinearLayout::horizontal()
358 .child(disk_select_view)
359 .child(DummyView.fixed_width(3))
360 .child(options_view);
361
362 Self {
363 view: LinearLayout::vertical().child(view),
364 phantom: PhantomData,
365 }
366 }
367
368 fn top_panel(mut self, view: impl View) -> Self {
369 if self.has_top_panel() {
370 self.view.remove_child(0);
371 }
372
373 self.view.insert_child(0, Panel::new(view));
374 self
375 }
376
377 ///
378 /// This function returns a tuple of vectors. The first vector contains the currently selected
379 /// disks in order of their selection slot. Empty slots are filtered out. The second vector
380 /// contains indices of each slot's selection, which enables us to restore the selection even
381 /// for empty slots.
382 ///
383 fn get_disks_and_selection(&mut self) -> Option<(Vec<Disk>, Vec<usize>)> {
384 let mut disks = vec![];
385 let view_top_index = usize::from(self.has_top_panel());
386
387 let disk_form = self
388 .view
389 .get_child_mut(view_top_index)?
390 .downcast_mut::<LinearLayout>()?
391 .get_child_mut(0)?
392 .downcast_mut::<LinearLayout>()?
393 .get_child_mut(2)?
394 .downcast_mut::<ScrollView<NamedView<FormView>>>()?
395 .get_inner_mut()
396 .get_mut();
397
398 let mut selected_disks = Vec::new();
399
400 for i in 0..disk_form.len() {
401 let disk = disk_form.get_value::<SelectView<Option<Disk>>, _>(i)?;
402
403 // `None` means no disk was selected for this slot
404 if let Some(disk) = disk {
405 disks.push(disk);
406 }
407
408 selected_disks.push(
409 disk_form
410 .get_child::<SelectView<Option<Disk>>>(i)?
411 .selected_id()?,
412 );
413 }
414
415 Some((disks, selected_disks))
416 }
417
418 fn inner_mut(&mut self) -> Option<&mut T> {
419 let view_top_index = usize::from(self.has_top_panel());
420
421 self.view
422 .get_child_mut(view_top_index)?
423 .downcast_mut::<LinearLayout>()?
424 .get_child_mut(2)?
425 .downcast_mut::<LinearLayout>()?
426 .get_child_mut(2)?
427 .downcast_mut::<T>()
428 }
429
430 fn has_top_panel(&self) -> bool {
431 // The root view should only ever have one or two children
432 assert!([1, 2].contains(&self.view.len()));
433
434 self.view.len() == 2
435 }
436 }
437
438 impl<T: 'static> ViewWrapper for MultiDiskOptionsView<T> {
439 cursive::wrap_impl!(self.view: LinearLayout);
440 }
441
442 struct BtrfsBootdiskOptionsView {
443 view: MultiDiskOptionsView<FormView>,
444 }
445
446 impl BtrfsBootdiskOptionsView {
447 fn new(disks: &[Disk], options: &BtrfsBootdiskOptions) -> Self {
448 let view = MultiDiskOptionsView::new(
449 disks,
450 &options.selected_disks,
451 FormView::new().child("hdsize", DiskSizeEditView::new().content(options.disk_size)),
452 )
453 .top_panel(TextView::new("Btrfs integration is a technology preview!").center());
454
455 Self { view }
456 }
457
458 fn get_values(&mut self) -> Option<(Vec<Disk>, BtrfsBootdiskOptions)> {
459 let (disks, selected_disks) = self.view.get_disks_and_selection()?;
460 let disk_size = self.view.inner_mut()?.get_value::<DiskSizeEditView, _>(0)?;
461
462 Some((
463 disks,
464 BtrfsBootdiskOptions {
465 disk_size,
466 selected_disks,
467 },
468 ))
469 }
470 }
471
472 impl ViewWrapper for BtrfsBootdiskOptionsView {
473 cursive::wrap_impl!(self.view: MultiDiskOptionsView<FormView>);
474 }
475
476 struct ZfsBootdiskOptionsView {
477 view: MultiDiskOptionsView<FormView>,
478 }
479
480 impl ZfsBootdiskOptionsView {
481 // TODO: Re-apply previous disk selection from `options` correctly
482 fn new(disks: &[Disk], options: &ZfsBootdiskOptions) -> Self {
483 let inner = FormView::new()
484 .child("ashift", IntegerEditView::new().content(options.ashift))
485 .child(
486 "compress",
487 SelectView::new()
488 .popup()
489 .with_all(ZFS_COMPRESS_OPTIONS.iter().map(|o| (o.to_string(), *o)))
490 .selected(
491 ZFS_COMPRESS_OPTIONS
492 .iter()
493 .position(|o| *o == options.compress)
494 .unwrap_or_default(),
495 ),
496 )
497 .child(
498 "checksum",
499 SelectView::new()
500 .popup()
501 .with_all(ZFS_CHECKSUM_OPTIONS.iter().map(|o| (o.to_string(), *o)))
502 .selected(
503 ZFS_CHECKSUM_OPTIONS
504 .iter()
505 .position(|o| *o == options.checksum)
506 .unwrap_or_default(),
507 ),
508 )
509 .child("copies", IntegerEditView::new().content(options.copies))
510 .child("hdsize", DiskSizeEditView::new().content(options.disk_size));
511
512 let view = MultiDiskOptionsView::new(disks, &options.selected_disks, inner)
513 .top_panel(TextView::new(
514 "ZFS is not compatible with hardware RAID controllers, for details see the documentation."
515 ).center());
516
517 Self { view }
518 }
519
520 fn get_values(&mut self) -> Option<(Vec<Disk>, ZfsBootdiskOptions)> {
521 let (disks, selected_disks) = self.view.get_disks_and_selection()?;
522 let view = self.view.inner_mut()?;
523
524 let ashift = view.get_value::<IntegerEditView, _>(0)?;
525 let compress = view.get_value::<SelectView<_>, _>(1)?;
526 let checksum = view.get_value::<SelectView<_>, _>(2)?;
527 let copies = view.get_value::<IntegerEditView, _>(3)?;
528 let disk_size = view.get_value::<DiskSizeEditView, _>(4)?;
529
530 Some((
531 disks,
532 ZfsBootdiskOptions {
533 ashift,
534 compress,
535 checksum,
536 copies,
537 disk_size,
538 selected_disks,
539 },
540 ))
541 }
542 }
543
544 impl ViewWrapper for ZfsBootdiskOptionsView {
545 cursive::wrap_impl!(self.view: MultiDiskOptionsView<FormView>);
546 }
547
548 fn advanced_options_view(disks: &[Disk], options: Rc<RefCell<BootdiskOptions>>) -> impl View {
549 Dialog::around(AdvancedBootdiskOptionsView::new(
550 disks,
551 &(*options).borrow(),
552 ))
553 .title("Advanced bootdisk options")
554 .button("Ok", {
555 let options_ref = options.clone();
556 move |siv| {
557 let runinfo = siv
558 .user_data::<InstallerState>()
559 .unwrap()
560 .runtime_info
561 .clone();
562
563 let options = siv
564 .call_on_name("advanced-bootdisk-options-dialog", |view: &mut Dialog| {
565 view.get_content_mut()
566 .downcast_mut::<AdvancedBootdiskOptionsView>()
567 .map(|v| v.get_values(&runinfo))
568 })
569 .flatten();
570
571 let options = match options {
572 Some(Ok(options)) => options,
573 Some(Err(err)) => {
574 siv.add_layer(Dialog::info(err));
575 return;
576 }
577 None => {
578 siv.add_layer(Dialog::info("Failed to retrieve bootdisk options view"));
579 return;
580 }
581 };
582
583 if let Err(duplicate) = check_for_duplicate_disks(&options.disks) {
584 siv.add_layer(Dialog::info(format!(
585 "Cannot select same disk twice: {duplicate}"
586 )));
587 return;
588 }
589
590 siv.pop_layer();
591 *(*options_ref).borrow_mut() = options;
592 }
593 })
594 .with_name("advanced-bootdisk-options-dialog")
595 .max_size((120, 40))
596 }
597
598 /// Checks a list of disks for duplicate entries, using their index as key.
599 ///
600 /// # Arguments
601 ///
602 /// * `disks` - A list of disks to check for duplicates.
603 fn check_for_duplicate_disks(disks: &[Disk]) -> Result<(), &Disk> {
604 let mut set = HashSet::new();
605
606 for disk in disks {
607 if !set.insert(&disk.index) {
608 return Err(disk);
609 }
610 }
611
612 Ok(())
613 }
614
615 /// Simple wrapper which returns an descriptive error if the list of disks is too short.
616 ///
617 /// # Arguments
618 ///
619 /// * `disks` - A list of disks to check the lenght of.
620 /// * `min` - Minimum number of disks
621 fn check_raid_min_disks(disks: &[Disk], min: usize) -> Result<(), String> {
622 if disks.len() < min {
623 Err(format!("Need at least {min} disks"))
624 } else {
625 Ok(())
626 }
627 }
628
629 /// Checks whether a user-supplied ZFS RAID setup is valid or not, such as disk sizes, minimum
630 /// number of disks and legacy BIOS compatibility.
631 ///
632 /// # Arguments
633 ///
634 /// * `runinfo` - `RuntimeInfo` instance of currently running system
635 /// * `level` - The targeted ZFS RAID level by the user.
636 /// * `disks` - List of disks designated as RAID targets.
637 fn check_zfs_raid_config(
638 runinfo: &RuntimeInfo,
639 level: ZfsRaidLevel,
640 disks: &[Disk],
641 ) -> Result<(), String> {
642 // See also Proxmox/Install.pm:get_zfs_raid_setup()
643
644 for disk in disks {
645 if runinfo.boot_type != BootType::Efi
646 && disk.block_size.map(|v| v == 4096).unwrap_or_default()
647 {
648 return Err("Booting from 4Kn drive in legacy BIOS mode is not supported.".to_owned());
649 }
650 }
651
652 let check_mirror_size = |disk1: &Disk, disk2: &Disk| {
653 if (disk1.size - disk2.size).abs() > disk1.size / 10. {
654 Err(format!(
655 "Mirrored disks must have same size:\n\n * {disk1}\n * {disk2}"
656 ))
657 } else {
658 Ok(())
659 }
660 };
661
662 match level {
663 ZfsRaidLevel::Raid0 => check_raid_min_disks(disks, 1)?,
664 ZfsRaidLevel::Raid1 => {
665 check_raid_min_disks(disks, 2)?;
666 for disk in disks {
667 check_mirror_size(&disks[0], disk)?;
668 }
669 }
670 ZfsRaidLevel::Raid10 => {
671 check_raid_min_disks(disks, 4)?;
672 // Pairs need to have the same size
673 for i in (0..disks.len()).step_by(2) {
674 check_mirror_size(&disks[i], &disks[i + 1])?;
675 }
676 }
677 // For RAID-Z: minimum disks number is level + 2
678 ZfsRaidLevel::RaidZ => {
679 check_raid_min_disks(disks, 3)?;
680 for disk in disks {
681 check_mirror_size(&disks[0], disk)?;
682 }
683 }
684 ZfsRaidLevel::RaidZ2 => {
685 check_raid_min_disks(disks, 4)?;
686 for disk in disks {
687 check_mirror_size(&disks[0], disk)?;
688 }
689 }
690 ZfsRaidLevel::RaidZ3 => {
691 check_raid_min_disks(disks, 5)?;
692 for disk in disks {
693 check_mirror_size(&disks[0], disk)?;
694 }
695 }
696 }
697
698 Ok(())
699 }
700
701 /// Checks whether a user-supplied Btrfs RAID setup is valid or not, such as minimum
702 /// number of disks.
703 ///
704 /// # Arguments
705 ///
706 /// * `level` - The targeted Btrfs RAID level by the user.
707 /// * `disks` - List of disks designated as RAID targets.
708 fn check_btrfs_raid_config(level: BtrfsRaidLevel, disks: &[Disk]) -> Result<(), String> {
709 // See also Proxmox/Install.pm:get_btrfs_raid_setup()
710
711 match level {
712 BtrfsRaidLevel::Raid0 => check_raid_min_disks(disks, 1)?,
713 BtrfsRaidLevel::Raid1 => check_raid_min_disks(disks, 2)?,
714 BtrfsRaidLevel::Raid10 => check_raid_min_disks(disks, 4)?,
715 }
716
717 Ok(())
718 }
719
720 #[cfg(test)]
721 mod tests {
722 use std::collections::HashMap;
723
724 use super::*;
725 use crate::setup::{Dns, NetworkInfo};
726
727 fn dummy_disk(index: usize) -> Disk {
728 Disk {
729 index: index.to_string(),
730 path: format!("/dev/dummy{index}"),
731 model: Some("Dummy disk".to_owned()),
732 size: 1024. * 1024. * 1024. * 8.,
733 block_size: Some(512),
734 }
735 }
736
737 fn dummy_disks(num: usize) -> Vec<Disk> {
738 (0..num).map(dummy_disk).collect()
739 }
740
741 fn dummy_runinfo(boot_type: BootType) -> RuntimeInfo {
742 RuntimeInfo {
743 boot_type,
744 country: Some("at".to_owned()),
745 disks: dummy_disks(4),
746 network: NetworkInfo {
747 dns: Dns {
748 domain: None,
749 dns: vec![],
750 },
751 routes: None,
752 interfaces: HashMap::new(),
753 },
754 total_memory: 1024 * 1024 * 1024 * 64,
755 hvm_supported: true,
756 }
757 }
758
759 #[test]
760 fn duplicate_disks() {
761 assert!(check_for_duplicate_disks(&dummy_disks(2)).is_ok());
762 assert_eq!(
763 check_for_duplicate_disks(&[
764 dummy_disk(0),
765 dummy_disk(1),
766 dummy_disk(2),
767 dummy_disk(2),
768 dummy_disk(3),
769 ]),
770 Err(&dummy_disk(2)),
771 );
772 }
773
774 #[test]
775 fn raid_min_disks() {
776 let disks = dummy_disks(10);
777
778 assert!(check_raid_min_disks(&disks[..1], 2).is_err());
779 assert!(check_raid_min_disks(&disks[..1], 1).is_ok());
780 assert!(check_raid_min_disks(&disks, 1).is_ok());
781 }
782
783 #[test]
784 fn btrfs_raid() {
785 let disks = dummy_disks(10);
786
787 assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid0, &[]).is_err());
788 assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid0, &disks[..1]).is_ok());
789 assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid0, &disks).is_ok());
790
791 assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid1, &[]).is_err());
792 assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid1, &disks[..1]).is_err());
793 assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid1, &disks[..2]).is_ok());
794 assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid1, &disks).is_ok());
795
796 assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid10, &[]).is_err());
797 assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid10, &disks[..3]).is_err());
798 assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid10, &disks[..4]).is_ok());
799 assert!(check_btrfs_raid_config(BtrfsRaidLevel::Raid10, &disks).is_ok());
800 }
801
802 #[test]
803 fn zfs_raid_bios() {
804 let runinfo = dummy_runinfo(BootType::Bios);
805
806 let mut disks = dummy_disks(10);
807 zfs_common_tests(&disks, &runinfo);
808
809 for disk in &mut disks {
810 disk.block_size = None;
811 }
812 // Should behave the same as if an explicit block size of 512 was set
813 zfs_common_tests(&disks, &runinfo);
814
815 for i in 0..10 {
816 let mut disks = dummy_disks(10);
817 disks[i].block_size = Some(4096);
818
819 // Must fail if /any/ of the disks are 4Kn
820 assert!(check_zfs_raid_config(&runinfo, ZfsRaidLevel::Raid0, &disks).is_err());
821 assert!(check_zfs_raid_config(&runinfo, ZfsRaidLevel::Raid1, &disks).is_err());
822 assert!(check_zfs_raid_config(&runinfo, ZfsRaidLevel::Raid10, &disks).is_err());
823 assert!(check_zfs_raid_config(&runinfo, ZfsRaidLevel::RaidZ, &disks).is_err());
824 assert!(check_zfs_raid_config(&runinfo, ZfsRaidLevel::RaidZ2, &disks).is_err());
825 assert!(check_zfs_raid_config(&runinfo, ZfsRaidLevel::RaidZ3, &disks).is_err());
826 }
827 }
828
829 #[test]
830 fn zfs_raid_efi() {
831 let disks = dummy_disks(10);
832 let runinfo = dummy_runinfo(BootType::Efi);
833
834 zfs_common_tests(&disks, &runinfo);
835 }
836
837 fn zfs_common_tests(disks: &[Disk], runinfo: &RuntimeInfo) {
838 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::Raid0, &[]).is_err());
839 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::Raid0, &disks[..1]).is_ok());
840 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::Raid0, disks).is_ok());
841
842 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::Raid1, &[]).is_err());
843 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::Raid1, &disks[..2]).is_ok());
844 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::Raid1, disks).is_ok());
845
846 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::Raid10, &[]).is_err());
847 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::Raid10, &dummy_disks(4)).is_ok());
848 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::Raid10, disks).is_ok());
849
850 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::RaidZ, &[]).is_err());
851 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::RaidZ, &disks[..2]).is_err());
852 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::RaidZ, &disks[..3]).is_ok());
853 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::RaidZ, disks).is_ok());
854
855 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::RaidZ2, &[]).is_err());
856 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::RaidZ2, &disks[..3]).is_err());
857 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::RaidZ2, &disks[..4]).is_ok());
858 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::RaidZ2, disks).is_ok());
859
860 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::RaidZ3, &[]).is_err());
861 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::RaidZ3, &disks[..4]).is_err());
862 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::RaidZ3, &disks[..5]).is_ok());
863 assert!(check_zfs_raid_config(runinfo, ZfsRaidLevel::RaidZ3, disks).is_ok());
864 }
865 }