#![forbid(unsafe_code)]
-mod views;
+use std::{collections::HashMap, env, net::IpAddr};
-use crate::views::DiskSizeFormInputView;
use cursive::{
event::Event,
- view::{Finder, Nameable, Resizable, ViewWrapper},
+ theme::{ColorStyle, Effect, PaletteColor, Style},
+ view::{Nameable, Offset, Resizable, ViewWrapper},
views::{
- Button, Dialog, DummyView, EditView, LinearLayout, PaddedView, Panel, ResizedView,
- ScrollView, SelectView, TextView,
+ Button, Checkbox, Dialog, DummyView, EditView, Layer, LinearLayout, PaddedView, Panel,
+ ResizedView, ScrollView, SelectView, StackView, TextView, ViewRef,
},
- Cursive, View,
+ Cursive, CursiveRunnable, ScreenId, View, XY,
};
-use std::fmt;
-use views::FormInputView;
-// TextView::center() seems to garble the first two lines, so fix it manually here.
-const LOGO: &str = r#"
- ____ _ __ _____
- / __ \_________ _ ______ ___ ____ _ __ | | / / ____/
- / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/ | | / / __/
- / ____/ / / /_/ /> </ / / / / / /_/ /> < | |/ / /___
-/_/ /_/ \____/_/|_/_/ /_/ /_/\____/_/|_| |___/_____/
-"#;
+use regex::Regex;
+
+mod options;
+use options::InstallerOptions;
+
+use proxmox_installer_common::{
+ options::{BootdiskOptions, NetworkOptions, PasswordOptions, TimezoneOptions},
+ setup::{installer_setup, LocaleInfo, ProxmoxProduct, RuntimeInfo, SetupInfo},
+ utils::Fqdn,
+};
+
+mod setup;
+
+mod system;
+
+mod views;
+use views::{
+ BootdiskOptionsView, CidrAddressEditView, FormView, InstallProgressView, TableView,
+ TableViewItem, TimezoneOptionsView,
+};
-const TITLE: &str = "Proxmox VE Installer";
+// TextView::center() seems to garble the first two lines, so fix it manually here.
+const PROXMOX_LOGO: &str = r#"
+ ____
+| _ \ _ __ _____ ___ __ ___ _____ __
+| |_) | '__/ _ \ \/ / '_ ` _ \ / _ \ \/ /
+| __/| | | (_) > <| | | | | | (_) > <
+|_| |_| \___/_/\_\_| |_| |_|\___/_/\_\ "#;
struct InstallerView {
- view: ResizedView<LinearLayout>,
+ view: ResizedView<Dialog>,
}
impl InstallerView {
- pub fn new<T: View>(view: T, next_cb: Box<dyn Fn(&mut Cursive)>) -> Self {
- let inner = LinearLayout::vertical().child(view).child(PaddedView::lrtb(
- 1,
- 1,
- 1,
- 0,
- LinearLayout::horizontal()
- .child(abort_install_button())
- .child(DummyView.full_width())
- .child(Button::new("Previous", switch_to_prev_screen))
- .child(DummyView)
- .child(Button::new("Next", next_cb)),
- ));
+ pub fn new<T: View>(
+ state: &InstallerState,
+ view: T,
+ next_cb: Box<dyn Fn(&mut Cursive)>,
+ focus_next: bool,
+ ) -> Self {
+ let mut bbar = LinearLayout::horizontal()
+ .child(abort_install_button())
+ .child(DummyView.full_width())
+ .child(Button::new("Previous", switch_to_prev_screen))
+ .child(DummyView)
+ .child(Button::new("Next", next_cb));
+ let _ = bbar.set_focus_index(4); // ignore errors
+ let mut inner = LinearLayout::vertical()
+ .child(PaddedView::lrtb(0, 0, 1, 1, view))
+ .child(PaddedView::lrtb(1, 1, 0, 0, bbar));
+ if focus_next {
+ let _ = inner.set_focus_index(1); // ignore errors
+ }
- Self::with_raw(inner)
+ Self::with_raw(state, inner)
}
- pub fn with_raw<T: View>(view: T) -> Self {
- let inner = LinearLayout::vertical()
- .child(PaddedView::lrtb(1, 1, 0, 1, TextView::new(LOGO).center()))
- .child(Dialog::around(view).title(TITLE));
+ pub fn with_raw(state: &InstallerState, view: impl View) -> Self {
+ let setup = &state.setup_info;
+
+ let title = format!(
+ "{} ({}-{}) Installer",
+ setup.config.fullname, setup.iso_info.release, setup.iso_info.isorelease
+ );
+
+ let inner = Dialog::around(view).title(title);
Self {
// Limit the maximum to something reasonable, such that it won't get spread out much
}
impl ViewWrapper for InstallerView {
- cursive::wrap_impl!(self.view: ResizedView<LinearLayout>);
+ cursive::wrap_impl!(self.view: ResizedView<Dialog>);
}
-#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
-enum FsType {
- #[default]
- Ext4,
- Xfs,
+struct InstallerBackgroundView {
+ view: StackView,
}
-impl fmt::Display for FsType {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- let s = match self {
- FsType::Ext4 => "ext4",
- FsType::Xfs => "XFS",
+impl InstallerBackgroundView {
+ pub fn new() -> Self {
+ let style = Style {
+ effects: Effect::Bold.into(),
+ color: ColorStyle::back(PaletteColor::View),
};
- write!(f, "{s}")
- }
-}
-
-const FS_TYPES: &[FsType] = &[FsType::Ext4, FsType::Xfs];
-#[derive(Clone, Debug)]
-struct LvmBootdiskOptions {
- disk: Disk,
- total_size: u64,
- swap_size: u64,
- max_root_size: u64,
- max_data_size: u64,
- min_lvm_free: u64,
-}
-
-impl LvmBootdiskOptions {
- fn defaults_from(disk: &Disk) -> Self {
- let min_lvm_free = if disk.size > 128 * 1024 * 1024 {
- 16 * 1024 * 1024
- } else {
- disk.size / 8
- };
+ let mut view = StackView::new();
+ view.add_fullscreen_layer(Layer::with_color(
+ DummyView
+ .full_width()
+ .fixed_height(PROXMOX_LOGO.lines().count() + 1),
+ ColorStyle::back(PaletteColor::View),
+ ));
+ view.add_transparent_layer_at(
+ XY {
+ x: Offset::Center,
+ y: Offset::Absolute(0),
+ },
+ TextView::new(PROXMOX_LOGO).style(style),
+ );
- Self {
- disk: disk.clone(),
- total_size: disk.size,
- swap_size: 4 * 1024 * 1024, // TODO: value from installed memory
- max_root_size: 0,
- max_data_size: 0,
- min_lvm_free,
- }
+ Self { view }
}
}
-#[derive(Clone, Debug)]
-enum AdvancedBootdiskOptions {
- Lvm(LvmBootdiskOptions),
-}
-
-#[derive(Clone, Debug)]
-struct Disk {
- path: String,
- size: u64,
+impl ViewWrapper for InstallerBackgroundView {
+ cursive::wrap_impl!(self.view: StackView);
}
-impl fmt::Display for Disk {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- // TODO: Format sizes properly with `proxmox-human-byte` once merged
- // https://lists.proxmox.com/pipermail/pbs-devel/2023-May/006125.html
- write!(f, "{} ({} B)", self.path, self.size)
- }
+#[derive(Clone, Eq, Hash, PartialEq)]
+enum InstallerStep {
+ Licence,
+ Bootdisk,
+ Timezone,
+ Password,
+ Network,
+ Summary,
+ Install,
}
-#[derive(Clone, Debug)]
-struct BootdiskOptions {
- disks: Vec<Disk>,
- fstype: FsType,
- advanced: AdvancedBootdiskOptions,
+#[derive(Clone)]
+struct InstallerState {
+ options: InstallerOptions,
+ setup_info: SetupInfo,
+ runtime_info: RuntimeInfo,
+ locales: LocaleInfo,
+ steps: HashMap<InstallerStep, ScreenId>,
+ in_test_mode: bool,
}
-#[derive(Clone, Debug)]
-struct TimezoneOptions {
- timezone: String,
- kb_layout: String,
-}
+fn main() {
+ let mut siv = cursive::termion();
-impl Default for TimezoneOptions {
- fn default() -> Self {
- Self {
- timezone: "Europe/Vienna".to_owned(),
- kb_layout: "en_US".to_owned(),
- }
- }
-}
+ let in_test_mode = match env::args().nth(1).as_deref() {
+ Some("-t") => true,
-#[derive(Clone, Debug)]
-struct InstallerOptions {
- bootdisk: BootdiskOptions,
- timezone: TimezoneOptions,
-}
+ // Always force the test directory in debug builds
+ _ => cfg!(debug_assertions),
+ };
-fn main() {
- let mut siv = cursive::termion();
+ let (setup_info, locales, runtime_info) = match installer_setup(in_test_mode) {
+ Ok(result) => result,
+ Err(err) => initial_setup_error(&mut siv, &err),
+ };
siv.clear_global_callbacks(Event::CtrlChar('c'));
siv.set_on_pre_event(Event::CtrlChar('c'), trigger_abort_install_dialog);
- let disks = vec![Disk {
- path: "/dev/vda".to_owned(),
- size: 17179869184,
- }];
- siv.set_user_data(InstallerOptions {
- bootdisk: BootdiskOptions {
- disks: disks.clone(),
- fstype: FsType::default(),
- advanced: AdvancedBootdiskOptions::Lvm(LvmBootdiskOptions::defaults_from(&disks[0])),
+ siv.set_user_data(InstallerState {
+ options: InstallerOptions {
+ bootdisk: BootdiskOptions::defaults_from(&runtime_info.disks[0]),
+ timezone: TimezoneOptions::defaults_from(&runtime_info, &locales),
+ password: Default::default(),
+ network: NetworkOptions::defaults_from(&setup_info, &runtime_info.network),
+ autoreboot: false,
},
- timezone: TimezoneOptions::default(),
+ setup_info,
+ runtime_info,
+ locales,
+ steps: HashMap::new(),
+ in_test_mode,
});
- siv.add_active_screen();
- siv.screen_mut().add_layer(license_dialog());
+ switch_to_next_screen(&mut siv, InstallerStep::Licence, &license_dialog);
+ siv.run();
+}
+
+/// Anything that can be done late in the setup and will not result in fatal errors.
+fn installer_setup_late(siv: &mut Cursive) {
+ let state = siv.user_data::<InstallerState>().cloned().unwrap();
+
+ if !state.in_test_mode {
+ let kmap_id = &state.options.timezone.kb_layout;
+ if let Some(kmap) = state.locales.kmap.get(kmap_id) {
+ if let Err(err) = system::set_keyboard_layout(kmap) {
+ display_setup_warning(siv, &format!("Failed to apply keyboard layout: {err}"));
+ }
+ }
+ }
+
+ if state.runtime_info.total_memory < 1024 {
+ display_setup_warning(
+ siv,
+ concat!(
+ "Less than 1 GiB of usable memory detected, installation will probably fail.\n\n",
+ "See 'System Requirements' in the documentation."
+ ),
+ );
+ }
+
+ if state.setup_info.config.product == ProxmoxProduct::PVE && !state.runtime_info.hvm_supported {
+ display_setup_warning(
+ siv,
+ concat!(
+ "No support for hardware-accelerated KVM virtualization detected.\n\n",
+ "Check BIOS settings for Intel VT / AMD-V / SVM."
+ ),
+ );
+ }
+}
+
+fn initial_setup_error(siv: &mut CursiveRunnable, message: &str) -> ! {
+ siv.add_layer(
+ Dialog::around(TextView::new(message))
+ .title("Installer setup error")
+ .button("Ok", Cursive::quit),
+ );
siv.run();
+
+ std::process::exit(1);
}
-fn add_next_screen(
+fn display_setup_warning(siv: &mut Cursive, message: &str) {
+ siv.add_layer(Dialog::info(message).title("Warning"));
+}
+
+fn switch_to_next_screen(
+ siv: &mut Cursive,
+ step: InstallerStep,
constructor: &dyn Fn(&mut Cursive) -> InstallerView,
-) -> Box<dyn Fn(&mut Cursive) + '_> {
- Box::new(|siv: &mut Cursive| {
- let v = constructor(siv);
- siv.add_active_screen();
- siv.screen_mut().add_layer(v);
- })
+) {
+ let state = siv.user_data::<InstallerState>().cloned().unwrap();
+ let is_first_screen = state.steps.is_empty();
+
+ // Check if the screen already exists; if yes, then simply switch to it.
+ if let Some(screen_id) = state.steps.get(&step) {
+ siv.set_screen(*screen_id);
+
+ // The summary view cannot be cached (otherwise it would display stale values). Thus
+ // replace it if the screen is switched to.
+ // TODO: Could be done by e.g. having all the main dialog views implement some sort of
+ // .refresh(), which can be called if the view is switched to.
+ if step == InstallerStep::Summary {
+ let view = constructor(siv);
+ siv.screen_mut().pop_layer();
+ siv.screen_mut().add_layer(view);
+ }
+
+ return;
+ }
+
+ let v = constructor(siv);
+ let screen = siv.add_active_screen();
+ siv.with_user_data(|state: &mut InstallerState| state.steps.insert(step, screen));
+
+ siv.screen_mut().add_transparent_layer_at(
+ XY {
+ x: Offset::Parent(0),
+ y: Offset::Parent(0),
+ },
+ InstallerBackgroundView::new(),
+ );
+
+ siv.screen_mut().add_layer(v);
+
+ // If this is the first screen to be added, execute our late setup first.
+ // Needs to be done here, at the end, to ensure that any potential layers get added to
+ // the right screen and are on top.
+ if is_first_screen {
+ installer_setup_late(siv);
+ }
}
fn switch_to_prev_screen(siv: &mut Cursive) {
siv.set_screen(id);
}
-fn yes_no_dialog(
+fn prompt_dialog(
siv: &mut Cursive,
title: &str,
text: &str,
- callback: &'static dyn Fn(&mut Cursive),
+ yes_text: &str,
+ callback_yes: Box<dyn Fn(&mut Cursive)>,
+ no_text: &str,
+ callback_no: Box<dyn Fn(&mut Cursive)>,
) {
siv.add_layer(
Dialog::around(TextView::new(text))
.title(title)
- .dismiss_button("No")
- .button("Yes", callback),
+ .button(no_text, move |siv| {
+ siv.pop_layer();
+ callback_no(siv);
+ })
+ .button(yes_text, move |siv| {
+ siv.pop_layer();
+ callback_yes(siv);
+ }),
)
}
siv.quit();
#[cfg(not(debug_assertions))]
- yes_no_dialog(
+ prompt_dialog(
siv,
"Abort installation?",
"Are you sure you want to abort the installation?",
- &Cursive::quit,
+ "Yes",
+ Box::new(Cursive::quit),
+ "No",
+ Box::new(|_| {}),
)
}
Button::new("Abort", trigger_abort_install_dialog)
}
-fn get_eula() -> String {
- // TODO: properly using info from Proxmox::Install::Env::setup()
- std::fs::read_to_string("/cdrom/EULA")
+fn get_eula(setup: &SetupInfo) -> String {
+ let mut path = setup.locations.iso.clone();
+ path.push("EULA");
+
+ std::fs::read_to_string(path)
.unwrap_or_else(|_| "< Debug build - ignoring non-existing EULA >".to_owned())
}
-fn license_dialog() -> InstallerView {
- let inner = LinearLayout::vertical()
+fn license_dialog(siv: &mut Cursive) -> InstallerView {
+ let state = siv.user_data::<InstallerState>().unwrap();
+
+ let mut bbar = LinearLayout::horizontal()
+ .child(abort_install_button())
+ .child(DummyView.full_width())
+ .child(Button::new("I agree", |siv| {
+ switch_to_next_screen(siv, InstallerStep::Bootdisk, &bootdisk_dialog)
+ }));
+ let _ = bbar.set_focus_index(2); // ignore errors
+
+ let mut inner = LinearLayout::vertical()
.child(PaddedView::lrtb(
0,
0,
TextView::new("END USER LICENSE AGREEMENT (EULA)").center(),
))
.child(Panel::new(ScrollView::new(
- TextView::new(get_eula()).center(),
+ TextView::new(get_eula(&state.setup_info)).center(),
)))
- .child(PaddedView::lrtb(
- 1,
- 1,
- 1,
- 0,
- LinearLayout::horizontal()
- .child(abort_install_button())
- .child(DummyView.full_width())
- .child(Button::new("I agree", add_next_screen(&bootdisk_dialog))),
- ));
+ .child(PaddedView::lrtb(1, 1, 1, 0, bbar));
+
+ let _ = inner.set_focus_index(2); // ignore errors
- InstallerView::with_raw(inner)
+ InstallerView::with_raw(state, inner)
}
fn bootdisk_dialog(siv: &mut Cursive) -> InstallerView {
- let options = siv
- .user_data::<InstallerOptions>()
- .map(|o| o.clone())
- .unwrap()
- .bootdisk;
+ let state = siv.user_data::<InstallerState>().cloned().unwrap();
+
+ InstallerView::new(
+ &state,
+ BootdiskOptionsView::new(siv, &state.runtime_info, &state.options.bootdisk)
+ .with_name("bootdisk-options"),
+ Box::new(|siv| {
+ let options = siv.call_on_name("bootdisk-options", BootdiskOptionsView::get_values);
- let AdvancedBootdiskOptions::Lvm(advanced) = options.advanced;
+ match options {
+ Some(Ok(options)) => {
+ siv.with_user_data(|state: &mut InstallerState| {
+ state.options.bootdisk = options;
+ });
- let fstype_select = LinearLayout::horizontal()
- .child(TextView::new("Filesystem: "))
- .child(DummyView.full_width())
- .child(
- SelectView::new()
- .popup()
- .with_all(FS_TYPES.iter().map(|t| (t.to_string(), t)))
- .selected(
- FS_TYPES
- .iter()
- .position(|t| *t == options.fstype)
- .unwrap_or_default(),
- )
- .on_submit({
- let disks = options.disks.clone();
- let advanced = advanced.clone();
- move |siv, fstype: &FsType| {
- let view = match fstype {
- FsType::Ext4 | FsType::Xfs => {
- LvmBootdiskOptionsView::new(&disks, &advanced)
- }
- };
-
- siv.call_on_name("bootdisk-options", |v: &mut LinearLayout| {
- v.clear();
- v.add_child(view);
- });
- }
- })
- .with_name("fstype")
- .full_width(),
- );
+ switch_to_next_screen(siv, InstallerStep::Timezone, &timezone_dialog);
+ }
- let inner = LinearLayout::vertical()
- .child(fstype_select)
- .child(DummyView)
+ Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
+ _ => siv.add_layer(Dialog::info("Invalid values")),
+ }
+ }),
+ true,
+ )
+}
+
+fn timezone_dialog(siv: &mut Cursive) -> InstallerView {
+ let state = siv.user_data::<InstallerState>().unwrap();
+ let options = &state.options.timezone;
+
+ InstallerView::new(
+ state,
+ TimezoneOptionsView::new(&state.locales, options).with_name("timezone-options"),
+ Box::new(|siv| {
+ let options = siv.call_on_name("timezone-options", TimezoneOptionsView::get_values);
+
+ match options {
+ Some(Ok(options)) => {
+ siv.with_user_data(|state: &mut InstallerState| {
+ state.options.timezone = options;
+ });
+
+ switch_to_next_screen(siv, InstallerStep::Password, &password_dialog);
+ }
+ Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
+ _ => siv.add_layer(Dialog::info("Invalid values")),
+ }
+ }),
+ true,
+ )
+}
+
+fn password_dialog(siv: &mut Cursive) -> InstallerView {
+ let state = siv.user_data::<InstallerState>().unwrap();
+ let options = &state.options.password;
+
+ let inner = FormView::new()
+ .child("Root password", EditView::new().secret())
+ .child("Confirm root password", EditView::new().secret())
.child(
- LinearLayout::horizontal()
- .child(LvmBootdiskOptionsView::new(&options.disks, &advanced))
- .with_name("bootdisk-options"),
- );
+ "Administrator email",
+ EditView::new().content(&options.email),
+ )
+ .with_name("password-options");
InstallerView::new(
+ state,
inner,
Box::new(|siv| {
- let options = siv
- .call_on_name("bootdisk-options", |v: &mut LinearLayout| {
- v.get_child_mut(0)?
- .downcast_mut::<LvmBootdiskOptionsView>()?
- .get_values()
- .map(AdvancedBootdiskOptions::Lvm)
- })
- .flatten()
- .unwrap();
-
- siv.with_user_data(|opts: &mut InstallerOptions| {
- opts.bootdisk.advanced = options;
+ let options = siv.call_on_name("password-options", |view: &mut FormView| {
+ let root_password = view
+ .get_value::<EditView, _>(0)
+ .ok_or("failed to retrieve password")?;
+
+ let confirm_password = view
+ .get_value::<EditView, _>(1)
+ .ok_or("failed to retrieve password confirmation")?;
+
+ let email = view
+ .get_value::<EditView, _>(2)
+ .ok_or("failed to retrieve email")?;
+
+ let email_regex =
+ Regex::new(r"^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$")
+ .unwrap();
+
+ if root_password.len() < 5 {
+ Err("password too short, must be at least 5 characters long")
+ } else if root_password != confirm_password {
+ Err("passwords do not match")
+ } else if email == "mail@example.invalid" {
+ Err("invalid email address")
+ } else if !email_regex.is_match(&email) {
+ Err("Email does not look like a valid address (user@domain.tld)")
+ } else {
+ Ok(PasswordOptions {
+ root_password,
+ email,
+ })
+ }
});
- add_next_screen(&timezone_dialog)(siv)
+ match options {
+ Some(Ok(options)) => {
+ siv.with_user_data(|state: &mut InstallerState| {
+ state.options.password = options;
+ });
+
+ switch_to_next_screen(siv, InstallerStep::Network, &network_dialog);
+ }
+ Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
+ _ => siv.add_layer(Dialog::info("Invalid values")),
+ }
}),
+ false,
)
}
-struct LvmBootdiskOptionsView {
- view: LinearLayout,
-}
+fn network_dialog(siv: &mut Cursive) -> InstallerView {
+ let state = siv.user_data::<InstallerState>().unwrap();
+ let options = &state.options.network;
+ let ifaces = state.runtime_info.network.interfaces.values();
+ let ifnames = ifaces
+ .clone()
+ .map(|iface| (iface.render(), iface.name.clone()));
+ let mut ifaces_selection = SelectView::new().popup().with_all(ifnames.clone());
+
+ ifaces_selection.sort();
+ ifaces_selection.set_selection(
+ ifnames
+ .clone()
+ .position(|iface| iface.1 == options.ifname)
+ .unwrap_or(ifaces.len() - 1),
+ );
+
+ let inner = FormView::new()
+ .child("Management interface", ifaces_selection)
+ .child(
+ "Hostname (FQDN)",
+ EditView::new().content(options.fqdn.to_string()),
+ )
+ .child(
+ "IP address (CIDR)",
+ CidrAddressEditView::new().content(options.address.clone()),
+ )
+ .child(
+ "Gateway address",
+ EditView::new().content(options.gateway.to_string()),
+ )
+ .child(
+ "DNS server address",
+ EditView::new().content(options.dns_server.to_string()),
+ )
+ .with_name("network-options");
-impl LvmBootdiskOptionsView {
- fn new(disks: &[Disk], options: &LvmBootdiskOptions) -> Self {
- let view = LinearLayout::vertical()
- .child(FormInputView::new(
- "Target harddisk",
- SelectView::new()
- .popup()
- .with_all(disks.iter().map(|d| (d.to_string(), d.clone())))
- .with_name("bootdisk-disk"),
- ))
- .child(DiskSizeFormInputView::new("Total size").content(options.total_size))
- .child(DiskSizeFormInputView::new("Swap size").content(options.swap_size))
- .child(
- DiskSizeFormInputView::new("Maximum root volume size")
- .content(options.max_root_size),
- )
- .child(
- DiskSizeFormInputView::new("Maximum data volume size")
- .content(options.max_data_size),
- )
- .child(
- DiskSizeFormInputView::new("Minimum free LVM space").content(options.min_lvm_free),
- );
+ InstallerView::new(
+ state,
+ inner,
+ Box::new(|siv| {
+ let options = siv.call_on_name("network-options", |view: &mut FormView| {
+ let ifname = view
+ .get_value::<SelectView, _>(0)
+ .ok_or("failed to retrieve management interface name")?;
+
+ let fqdn = view
+ .get_value::<EditView, _>(1)
+ .ok_or("failed to retrieve host FQDN")?
+ .parse::<Fqdn>()
+ .map_err(|err| format!("hostname does not look valid:\n\n{err}"))?;
+
+ let address = view
+ .get_value::<CidrAddressEditView, _>(2)
+ .ok_or("failed to retrieve host address")?;
+
+ let gateway = view
+ .get_value::<EditView, _>(3)
+ .ok_or("failed to retrieve gateway address")?
+ .parse::<IpAddr>()
+ .map_err(|err| err.to_string())?;
+
+ let dns_server = view
+ .get_value::<EditView, _>(4)
+ .ok_or("failed to retrieve DNS server address")?
+ .parse::<IpAddr>()
+ .map_err(|err| err.to_string())?;
+
+ if address.addr().is_ipv4() != gateway.is_ipv4() {
+ Err("host and gateway IP address version must not differ".to_owned())
+ } else if address.addr().is_ipv4() != dns_server.is_ipv4() {
+ Err("host and DNS IP address version must not differ".to_owned())
+ } else if fqdn.to_string().ends_with(".invalid") {
+ Err("hostname does not look valid".to_owned())
+ } else {
+ Ok(NetworkOptions {
+ ifname,
+ fqdn,
+ address,
+ gateway,
+ dns_server,
+ })
+ }
+ });
- Self { view }
- }
+ match options {
+ Some(Ok(options)) => {
+ siv.with_user_data(|state: &mut InstallerState| {
+ state.options.network = options;
+ });
+
+ switch_to_next_screen(siv, InstallerStep::Summary, &summary_dialog);
+ }
+ Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
+ _ => siv.add_layer(Dialog::info("Invalid values")),
+ }
+ }),
+ true,
+ )
+}
- fn get_values(&mut self) -> Option<LvmBootdiskOptions> {
- let disk = self
- .view
- .call_on_name("bootdisk-disk", |view: &mut SelectView<Disk>| {
- view.selection()
- })?
- .map(|d| (*d).clone())?;
-
- let mut get_disksize_value = |i| {
- self.view
- .get_child_mut(i)?
- .downcast_mut::<DiskSizeFormInputView>()?
- .get_content()
- };
+pub struct SummaryOption {
+ name: &'static str,
+ value: String,
+}
- Some(LvmBootdiskOptions {
- disk,
- total_size: get_disksize_value(1)?,
- swap_size: get_disksize_value(2)?,
- max_root_size: get_disksize_value(3)?,
- max_data_size: get_disksize_value(4)?,
- min_lvm_free: get_disksize_value(5)?,
- })
+impl SummaryOption {
+ pub fn new<S: Into<String>>(name: &'static str, value: S) -> Self {
+ Self {
+ name,
+ value: value.into(),
+ }
}
}
-impl ViewWrapper for LvmBootdiskOptionsView {
- cursive::wrap_impl!(self.view: LinearLayout);
+impl TableViewItem for SummaryOption {
+ fn get_column(&self, name: &str) -> String {
+ match name {
+ "name" => self.name.to_owned(),
+ "value" => self.value.clone(),
+ _ => unreachable!(),
+ }
+ }
}
-fn timezone_dialog(siv: &mut Cursive) -> InstallerView {
- let options = siv
- .user_data::<InstallerOptions>()
- .map(|o| o.timezone.clone())
- .unwrap_or_default();
-
- let inner = LinearLayout::vertical()
- .child(FormInputView::new(
- "Country",
- EditView::new().content("Austria"),
- ))
- .child(FormInputView::new(
- "Timezone",
- EditView::new()
- .content(options.timezone)
- .with_name("timezone-tzname"),
- ))
- .child(FormInputView::new(
- "Keyboard layout",
- EditView::new()
- .content(options.kb_layout)
- .with_name("timezone-kblayout"),
- ));
+fn summary_dialog(siv: &mut Cursive) -> InstallerView {
+ let state = siv.user_data::<InstallerState>().unwrap();
- InstallerView::new(
- inner,
- Box::new(|siv| {
- let timezone = siv
- .call_on_name("timezone-tzname", |v: &mut EditView| {
- (*v.get_content()).clone()
- })
- .unwrap();
-
- let kb_layout = siv
- .call_on_name("timezone-kblayout", |v: &mut EditView| {
- (*v.get_content()).clone()
- })
- .unwrap();
-
- siv.with_user_data(|opts: &mut InstallerOptions| {
- opts.timezone = TimezoneOptions {
- timezone,
- kb_layout,
- };
- dbg!(&opts.timezone);
+ let mut bbar = LinearLayout::horizontal()
+ .child(abort_install_button())
+ .child(DummyView.full_width())
+ .child(Button::new("Previous", switch_to_prev_screen))
+ .child(DummyView)
+ .child(Button::new("Install", |siv| {
+ let autoreboot = siv
+ .find_name("reboot-after-install")
+ .map(|v: ViewRef<Checkbox>| v.is_checked())
+ .unwrap_or_default();
+
+ siv.with_user_data(|state: &mut InstallerState| {
+ state.options.autoreboot = autoreboot;
});
- }),
- )
+
+ switch_to_next_screen(siv, InstallerStep::Install, &install_progress_dialog);
+ }));
+
+ let _ = bbar.set_focus_index(2); // ignore errors
+
+ let mut inner = LinearLayout::vertical()
+ .child(PaddedView::lrtb(
+ 0,
+ 0,
+ 1,
+ 2,
+ TableView::new()
+ .columns(&[
+ ("name".to_owned(), "Option".to_owned()),
+ ("value".to_owned(), "Selected value".to_owned()),
+ ])
+ .items(state.options.to_summary(&state.locales)),
+ ))
+ .child(
+ LinearLayout::horizontal()
+ .child(DummyView.full_width())
+ .child(Checkbox::new().checked().with_name("reboot-after-install"))
+ .child(
+ TextView::new(" Automatically reboot after successful installation").no_wrap(),
+ )
+ .child(DummyView.full_width()),
+ )
+ .child(PaddedView::lrtb(1, 1, 1, 0, bbar));
+
+ let _ = inner.set_focus_index(2); // ignore errors
+
+ InstallerView::with_raw(state, inner)
+}
+
+fn install_progress_dialog(siv: &mut Cursive) -> InstallerView {
+ // Ensure the screen is updated independently of keyboard events and such
+ siv.set_autorefresh(true);
+
+ let state = siv.user_data::<InstallerState>().cloned().unwrap();
+ InstallerView::with_raw(&state, InstallProgressView::new(siv))
}