]> git.proxmox.com Git - pve-installer.git/blobdiff - proxmox-tui-installer/src/main.rs
tui: install progress: use ok/cancel as button text for installer prompt
[pve-installer.git] / proxmox-tui-installer / src / main.rs
index 290877c5625b4e1712ac23f8c940b13064eb573d..4c1448248575facea743bd8725e76f69277501da 100644 (file)
@@ -1,49 +1,37 @@
 #![forbid(unsafe_code)]
 
-use std::{
-    collections::HashMap,
-    env,
-    io::{BufRead, BufReader, Write},
-    net::IpAddr,
-    path::PathBuf,
-    str::FromStr,
-    sync::{Arc, Mutex},
-    thread,
-    time::Duration,
-};
+use std::{collections::HashMap, env, net::IpAddr};
 
 use cursive::{
     event::Event,
     theme::{ColorStyle, Effect, PaletteColor, Style},
-    utils::Counter,
     view::{Nameable, Offset, Resizable, ViewWrapper},
     views::{
         Button, Checkbox, Dialog, DummyView, EditView, Layer, LinearLayout, PaddedView, Panel,
-        ProgressBar, ResizedView, ScrollView, SelectView, StackView, TextContent, TextView,
-        ViewRef,
+        ResizedView, ScrollView, SelectView, StackView, TextView, ViewRef,
     },
     Cursive, CursiveRunnable, ScreenId, View, XY,
 };
 
 use regex::Regex;
 
-use proxmox_sys::linux::procfs;
-
 mod options;
-use options::*;
+use options::InstallerOptions;
+
+use proxmox_installer_common::{
+    options::{BootdiskOptions, NetworkOptions, PasswordOptions, TimezoneOptions},
+    setup::{installer_setup, LocaleInfo, ProxmoxProduct, RuntimeInfo, SetupInfo},
+    utils::Fqdn,
+};
 
 mod setup;
-use setup::{InstallConfig, LocaleInfo, RuntimeInfo, SetupInfo};
 
 mod system;
 
-mod utils;
-use utils::Fqdn;
-
 mod views;
 use views::{
-    BootdiskOptionsView, CidrAddressEditView, FormView, TableView, TableViewItem,
-    TimezoneOptionsView,
+    BootdiskOptionsView, CidrAddressEditView, FormView, InstallProgressView, TableView,
+    TableViewItem, TimezoneOptionsView,
 };
 
 // TextView::center() seems to garble the first two lines, so fix it manually here.
@@ -182,7 +170,7 @@ fn main() {
             bootdisk: BootdiskOptions::defaults_from(&runtime_info.disks[0]),
             timezone: TimezoneOptions::defaults_from(&runtime_info, &locales),
             password: Default::default(),
-            network: NetworkOptions::from(&runtime_info.network),
+            network: NetworkOptions::defaults_from(&setup_info, &runtime_info.network),
             autoreboot: false,
         },
         setup_info,
@@ -196,48 +184,9 @@ fn main() {
     siv.run();
 }
 
-fn installer_setup(in_test_mode: bool) -> Result<(SetupInfo, LocaleInfo, RuntimeInfo), String> {
-    let base_path = if in_test_mode { "./testdir" } else { "/" };
-    let mut path = PathBuf::from(base_path);
-
-    path.push("run");
-    path.push("proxmox-installer");
-
-    let installer_info = {
-        let mut path = path.clone();
-        path.push("iso-info.json");
-
-        setup::read_json(&path).map_err(|err| format!("Failed to retrieve setup info: {err}"))?
-    };
-
-    let locale_info = {
-        let mut path = path.clone();
-        path.push("locales.json");
-
-        setup::read_json(&path).map_err(|err| format!("Failed to retrieve locale info: {err}"))?
-    };
-
-    let mut runtime_info: RuntimeInfo = {
-        let mut path = path.clone();
-        path.push("run-env-info.json");
-
-        setup::read_json(&path)
-            .map_err(|err| format!("Failed to retrieve runtime environment info: {err}"))?
-    };
-
-    system::has_min_requirements(&runtime_info)?;
-
-    runtime_info.disks.sort();
-    if runtime_info.disks.is_empty() {
-        Err("The installer could not find any supported hard disks.".to_owned())
-    } else {
-        Ok((installer_info, locale_info, runtime_info))
-    }
-}
-
 /// 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>().unwrap();
+    let state = siv.user_data::<InstallerState>().cloned().unwrap();
 
     if !state.in_test_mode {
         let kmap_id = &state.options.timezone.kb_layout;
@@ -248,16 +197,24 @@ fn installer_setup_late(siv: &mut Cursive) {
         }
     }
 
-    if let Ok(cpuinfo) = procfs::read_cpuinfo().map_err(|err| err.to_string()) {
-        if !cpuinfo.hvm {
-            display_setup_warning(
-                siv,
-                concat!(
-                    "No support for hardware-accelerated KVM virtualization detected.\n\n",
-                    "Check BIOS settings for Intel VT / AMD-V / SVM."
-                ),
-            );
-        }
+    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."
+            ),
+        );
     }
 }
 
@@ -328,23 +285,23 @@ 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_yes: &'static dyn Fn(&mut Cursive),
-    // callback_no: &'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)
-            .button("No", move |siv| {
+            .button(no_text, move |siv| {
                 siv.pop_layer();
                 callback_no(siv);
             })
-            .button("Yes", move |siv| {
+            .button(yes_text, move |siv| {
                 siv.pop_layer();
                 callback_yes(siv);
             }),
@@ -356,11 +313,13 @@ fn trigger_abort_install_dialog(siv: &mut Cursive) {
     siv.quit();
 
     #[cfg(not(debug_assertions))]
-    yes_no_dialog(
+    prompt_dialog(
         siv,
         "Abort installation?",
         "Are you sure you want to abort the installation?",
+        "Yes",
         Box::new(Cursive::quit),
+        "No",
         Box::new(|_| {}),
     )
 }
@@ -369,9 +328,11 @@ fn abort_install_button() -> Button {
     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())
 }
 
@@ -395,7 +356,7 @@ fn license_dialog(siv: &mut Cursive) -> InstallerView {
             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, bbar));
 
@@ -409,7 +370,7 @@ fn bootdisk_dialog(siv: &mut Cursive) -> InstallerView {
 
     InstallerView::new(
         &state,
-        BootdiskOptionsView::new(&state.runtime_info.disks, &state.options.bootdisk)
+        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);
@@ -465,7 +426,7 @@ fn password_dialog(siv: &mut Cursive) -> InstallerView {
         .child("Root password", EditView::new().secret())
         .child("Confirm root password", EditView::new().secret())
         .child(
-            "Administator email",
+            "Administrator email",
             EditView::new().content(&options.email),
         )
         .with_name("password-options");
@@ -492,7 +453,7 @@ fn password_dialog(siv: &mut Cursive) -> InstallerView {
                         .unwrap();
 
                 if root_password.len() < 5 {
-                    Err("password too short")
+                    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" {
@@ -526,14 +487,22 @@ fn password_dialog(siv: &mut Cursive) -> InstallerView {
 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",
-            SelectView::new()
-                .popup()
-                .with_all_str(state.runtime_info.network.interfaces.keys()),
-        )
+        .child("Management interface", ifaces_selection)
         .child(
             "Hostname (FQDN)",
             EditView::new().content(options.fqdn.to_string()),
@@ -565,7 +534,7 @@ fn network_dialog(siv: &mut Cursive) -> InstallerView {
                     .get_value::<EditView, _>(1)
                     .ok_or("failed to retrieve host FQDN")?
                     .parse::<Fqdn>()
-                    .map_err(|_| "failed to parse hostname".to_owned())?;
+                    .map_err(|err| format!("hostname does not look valid:\n\n{err}"))?;
 
                 let address = view
                     .get_value::<CidrAddressEditView, _>(2)
@@ -587,9 +556,6 @@ fn network_dialog(siv: &mut Cursive) -> InstallerView {
                     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().chars().all(|c| c.is_ascii_digit()) {
-                    // Not supported/allowed on Debian
-                    Err("hostname cannot be purely numeric".to_owned())
                 } else if fqdn.to_string().ends_with(".invalid") {
                     Err("hostname does not look valid".to_owned())
                 } else {
@@ -699,215 +665,6 @@ fn install_progress_dialog(siv: &mut Cursive) -> InstallerView {
     // Ensure the screen is updated independently of keyboard events and such
     siv.set_autorefresh(true);
 
-    let cb_sink = siv.cb_sink().clone();
-    let state = siv.user_data::<InstallerState>().unwrap();
-    let progress_text = TextContent::new("starting the installation ..");
-
-    let progress_task = {
-        let progress_text = progress_text.clone();
-        let options = state.options.clone();
-        move |counter: Counter| {
-            let child = {
-                use std::process::{Command, Stdio};
-
-                #[cfg(not(debug_assertions))]
-                let (path, args, envs): (&str, [&str; 1], [(&str, &str); 0]) =
-                    ("proxmox-low-level-installer", ["start-session"], []);
-
-                #[cfg(debug_assertions)]
-                let (path, args, envs) = (
-                    PathBuf::from("./proxmox-low-level-installer"),
-                    ["-t", "start-session-test"],
-                    [("PERL5LIB", ".")],
-                );
-
-                Command::new(path)
-                    .args(args)
-                    .envs(envs)
-                    .stdin(Stdio::piped())
-                    .stdout(Stdio::piped())
-                    .spawn()
-            };
-
-            let mut child = match child {
-                Ok(child) => child,
-                Err(err) => {
-                    let _ = cb_sink.send(Box::new(move |siv| {
-                        siv.add_layer(
-                            Dialog::text(err.to_string())
-                                .title("Error")
-                                .button("Ok", Cursive::quit),
-                        );
-                    }));
-                    return;
-                }
-            };
-
-            let inner = || {
-                let reader = child.stdout.take().map(BufReader::new)?;
-                let mut writer = child.stdin.take()?;
-
-                serde_json::to_writer(&mut writer, &InstallConfig::from(options)).unwrap();
-                writeln!(writer).unwrap();
-
-                let writer = Arc::new(Mutex::new(writer));
-
-                for line in reader.lines() {
-                    let line = match line {
-                        Ok(line) => line,
-                        Err(_) => break,
-                    };
-
-                    let msg = match line.parse::<UiMessage>() {
-                        Ok(msg) => msg,
-                        Err(stray) => {
-                            eprintln!("low-level installer: {stray}");
-                            continue;
-                        }
-                    };
-
-                    match msg {
-                        UiMessage::Info(s) => cb_sink.send(Box::new(|siv| {
-                            siv.add_layer(Dialog::info(s).title("Information"));
-                        })),
-                        UiMessage::Error(s) => cb_sink.send(Box::new(|siv| {
-                            siv.add_layer(Dialog::info(s).title("Error"));
-                        })),
-                        UiMessage::Prompt(s) => cb_sink.send({
-                            let writer = writer.clone();
-                            Box::new(move |siv| {
-                                yes_no_dialog(
-                                    siv,
-                                    "Prompt",
-                                    &s,
-                                    Box::new({
-                                        let writer = writer.clone();
-                                        move |_| {
-                                            if let Ok(mut writer) = writer.lock() {
-                                                let _ = writeln!(writer, "ok");
-                                            }
-                                        }
-                                    }),
-                                    Box::new(move |_| {
-                                        if let Ok(mut writer) = writer.lock() {
-                                            let _ = writeln!(writer);
-                                        }
-                                    }),
-                                );
-                            })
-                        }),
-                        UiMessage::Progress(ratio, s) => {
-                            counter.set(ratio);
-                            progress_text.set_content(s);
-                            Ok(())
-                        }
-                        UiMessage::Finished(success, msg) => {
-                            counter.set(100);
-                            progress_text.set_content(msg.to_owned());
-                            cb_sink.send(Box::new(move |siv| {
-                                let title = if success { "Success" } else { "Failure" };
-
-                                // For rebooting, we just need to quit the installer,
-                                // our caller does the actual reboot.
-                                siv.add_layer(
-                                    Dialog::text(msg)
-                                        .title(title)
-                                        .button("Reboot now", Cursive::quit),
-                                );
-
-                                let autoreboot = siv
-                                    .user_data::<InstallerState>()
-                                    .map(|state| state.options.autoreboot)
-                                    .unwrap_or_default();
-
-                                if autoreboot && success {
-                                    let cb_sink = siv.cb_sink();
-                                    thread::spawn({
-                                        let cb_sink = cb_sink.clone();
-                                        move || {
-                                            thread::sleep(Duration::from_secs(5));
-                                            let _ = cb_sink.send(Box::new(Cursive::quit));
-                                        }
-                                    });
-                                }
-                            }))
-                        }
-                    }
-                    .unwrap();
-                }
-
-                Some(())
-            };
-
-            if inner().is_none() {
-                cb_sink
-                    .send(Box::new(|siv| {
-                        siv.add_layer(
-                            Dialog::text("low-level installer exited early")
-                                .title("Error")
-                                .button("Exit", Cursive::quit),
-                        );
-                    }))
-                    .unwrap();
-            }
-        }
-    };
-
-    let progress_bar = ProgressBar::new().with_task(progress_task).full_width();
-    let inner = PaddedView::lrtb(
-        1,
-        1,
-        1,
-        1,
-        LinearLayout::vertical()
-            .child(PaddedView::lrtb(1, 1, 0, 0, progress_bar))
-            .child(DummyView)
-            .child(TextView::new_with_content(progress_text).center())
-            .child(PaddedView::lrtb(
-                1,
-                1,
-                1,
-                0,
-                LinearLayout::horizontal().child(abort_install_button()),
-            )),
-    );
-
-    InstallerView::with_raw(state, inner)
-}
-
-enum UiMessage {
-    Info(String),
-    Error(String),
-    Prompt(String),
-    Finished(bool, String),
-    Progress(usize, String),
-}
-
-impl FromStr for UiMessage {
-    type Err = String;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let (ty, rest) = s.split_once(": ").ok_or("invalid message: no type")?;
-
-        match ty {
-            "message" => Ok(UiMessage::Info(rest.to_owned())),
-            "error" => Ok(UiMessage::Error(rest.to_owned())),
-            "prompt" => Ok(UiMessage::Prompt(rest.to_owned())),
-            "finished" => {
-                let (state, rest) = rest.split_once(", ").ok_or("invalid message: no state")?;
-                Ok(UiMessage::Finished(state == "ok", rest.to_owned()))
-            }
-            "progress" => {
-                let (percent, rest) = rest.split_once(' ').ok_or("invalid progress message")?;
-                Ok(UiMessage::Progress(
-                    percent
-                        .parse::<f64>()
-                        .map(|v| (v * 100.).floor() as usize)
-                        .map_err(|err| err.to_string())?,
-                    rest.to_owned(),
-                ))
-            }
-            unknown => Err(format!("invalid message type {unknown}, rest: {rest}")),
-        }
-    }
+    let state = siv.user_data::<InstallerState>().cloned().unwrap();
+    InstallerView::with_raw(&state, InstallProgressView::new(siv))
 }