#![forbid(unsafe_code)]
-use std::{
- collections::HashMap,
- env,
- io::{BufRead, BufReader, Write},
- net::IpAddr,
- path::PathBuf,
- str::FromStr,
- sync::{Arc, Mutex},
-};
+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;
+
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.
const PROXMOX_LOGO: &str = r#"
- ____
- / __ \_________ _ ______ ___ ____ _ __
- / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/
- / ____/ / / /_/ /> </ / / / / / /_/ /> <
-/_/ /_/ \____/_/|_/_/ /_/ /_/\____/_/|_| "#;
+ ____
+| _ \ _ __ _____ ___ __ ___ _____ __
+| |_) | '__/ _ \ \/ / '_ ` _ \ / _ \ \/ /
+| __/| | | (_) > <| | | | | | (_) > <
+|_| |_| \___/_/\_\_| |_| |_|\___/_/\_\ "#;
struct InstallerView {
view: ResizedView<Dialog>,
state: &InstallerState,
view: T,
next_cb: Box<dyn Fn(&mut Cursive)>,
+ focus_next: bool,
) -> Self {
- let inner = LinearLayout::vertical()
+ 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,
- 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)),
- ));
+ .child(PaddedView::lrtb(1, 1, 0, 0, bbar));
+ if focus_next {
+ let _ = inner.set_focus_index(1); // ignore errors
+ }
Self::with_raw(state, inner)
}
options: InstallerOptions {
bootdisk: BootdiskOptions::defaults_from(&runtime_info.disks[0]),
timezone: TimezoneOptions::defaults_from(&runtime_info, &locales),
- password: PasswordOptions::defaults_from(&runtime_info),
- network: NetworkOptions::from(&runtime_info.network),
- reboot: false,
+ password: Default::default(),
+ network: NetworkOptions::defaults_from(&setup_info, &runtime_info.network),
+ autoreboot: false,
},
setup_info,
runtime_info,
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");
+/// 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();
- setup::read_json(&path)
- .map_err(|err| format!("Failed to retrieve runtime environment info: {err}"))?
- };
+ 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}"));
+ }
+ }
+ }
- system::has_min_requirements(&runtime_info)?;
+ 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."
+ ),
+ );
+ }
- 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))
+ 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."
+ ),
+ );
}
}
std::process::exit(1);
}
+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,
) {
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;
}
);
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_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);
}),
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(|_| {}),
)
}
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(siv: &mut Cursive) -> InstallerView {
let state = siv.user_data::<InstallerState>().unwrap();
- let inner = LinearLayout::vertical()
+ 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", |siv| {
- switch_to_next_screen(siv, InstallerStep::Bootdisk, &bootdisk_dialog)
- })),
- ));
+ .child(PaddedView::lrtb(1, 1, 1, 0, bbar));
+
+ let _ = inner.set_focus_index(2); // ignore errors
InstallerView::with_raw(state, inner)
}
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);
_ => siv.add_layer(Dialog::info("Invalid values")),
}
}),
+ true,
)
}
_ => siv.add_layer(Dialog::info("Invalid values")),
}
}),
+ true,
)
}
.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");
.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")
+ 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,
_ => siv.add_layer(Dialog::info("Invalid values")),
}
}),
+ false,
)
}
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()),
.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)
.map_err(|err| err.to_string())?;
let dns_server = view
- .get_value::<EditView, _>(3)
+ .get_value::<EditView, _>(4)
.ok_or("failed to retrieve DNS server address")?
.parse::<IpAddr>()
.map_err(|err| err.to_string())?;
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 {
_ => siv.add_layer(Dialog::info("Invalid values")),
}
}),
+ true,
)
}
fn summary_dialog(siv: &mut Cursive) -> InstallerView {
let state = siv.user_data::<InstallerState>().unwrap();
- let inner = LinearLayout::vertical()
+ 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,
.child(
LinearLayout::horizontal()
.child(DummyView.full_width())
- .child(Checkbox::new().with_name("reboot-after-install"))
+ .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,
- 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 reboot = 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.reboot = reboot;
- });
+ .child(PaddedView::lrtb(1, 1, 1, 0, bbar));
- switch_to_next_screen(siv, InstallerStep::Install, &install_progress_dialog);
- })),
- ));
+ let _ = inner.set_focus_index(2); // ignore errors
InstallerView::with_raw(state, inner)
}
// 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")
- .canonicalize()
- .unwrap(),
- ["-t", "start-session"],
- [("PERL5LIB", PathBuf::from("..").canonicalize().unwrap())],
- );
-
- 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" };
- siv.add_layer(
- Dialog::text(msg)
- .title(title)
- .button("Reboot", |s| s.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(),
- ))
- }
- _ => Err("invalid message type".to_owned()),
- }
- }
+ let state = siv.user_data::<InstallerState>().cloned().unwrap();
+ InstallerView::with_raw(&state, InstallProgressView::new(siv))
}