#![forbid(unsafe_code)]
-mod options;
-mod setup;
-mod system;
-mod utils;
-mod views;
+use std::{collections::HashMap, env, net::IpAddr};
-use crate::options::*;
use cursive::{
event::Event,
- view::{Nameable, Resizable, ViewWrapper},
+ theme::{ColorStyle, Effect, PaletteColor, Style},
+ view::{Nameable, Offset, Resizable, ViewWrapper},
views::{
- Button, Checkbox, Dialog, DummyView, EditView, LinearLayout, PaddedView, Panel,
- ProgressBar, ResizedView, ScrollView, SelectView, TextContent, TextView, ViewRef,
+ Button, Checkbox, Dialog, DummyView, EditView, Layer, LinearLayout, PaddedView, Panel,
+ ResizedView, ScrollView, SelectView, StackView, TextView, ViewRef,
},
- Cursive, CursiveRunnable, ScreenId, View,
+ Cursive, CursiveRunnable, ScreenId, View, XY,
};
-use setup::{LocaleInfo, ProxmoxProduct, SetupInfo};
-use std::{collections::HashMap, env, net::IpAddr};
-use utils::Fqdn;
+
+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, 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 LOGO_PVE: &str = r#"
- ____ _ __ _____
- / __ \_________ _ ______ ___ ____ _ __ | | / / ____/
- / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/ | | / / __/
- / ____/ / / /_/ /> </ / / / / / /_/ /> < | |/ / /___
-/_/ /_/ \____/_/|_/_/ /_/ /_/\____/_/|_| |___/_____/
-"#;
-
-const LOGO_PBS: &str = r#"
- ____ ____ _____
- / __ \_________ _ ______ ___ ____ _ __ / __ ) ___/
- / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/ / __ \__ \
- / ____/ / / /_/ /> </ / / / / / /_/ /> < / /_/ /__/ /
-/_/ /_/ \____/_/|_/_/ /_/ /_/\____/_/|_| /_____/____/
-"#;
-
-const LOGO_PMG: &str = r#"
- ____ __ _________
- / __ \_________ _ ______ ___ ____ _ __ / |/ / ____/
- / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/ / /|_/ / / __
- / ____/ / / /_/ /> </ / / / / / /_/ /> < / / / / /_/ /
-/_/ /_/ \____/_/|_/_/ /_/ /_/\____/_/|_| /_/ /_/\____/
-"#;
+const PROXMOX_LOGO: &str = r#"
+ ____
+| _ \ _ __ _____ ___ __ ___ _____ __
+| |_) | '__/ _ \ \/ / '_ ` _ \ / _ \ \/ /
+| __/| | | (_) > <| | | | | | (_) > <
+|_| |_| \___/_/\_\_| |_| |_|\___/_/\_\ "#;
struct InstallerView {
- view: ResizedView<LinearLayout>,
+ view: ResizedView<Dialog>,
}
impl InstallerView {
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)
}
pub fn with_raw(state: &InstallerState, view: impl View) -> Self {
let setup = &state.setup_info;
- let logo = match setup.config.product {
- ProxmoxProduct::PVE => LOGO_PVE,
- ProxmoxProduct::PBS => LOGO_PBS,
- ProxmoxProduct::PMG => LOGO_PMG,
- };
-
let title = format!(
"{} ({}-{}) Installer",
setup.config.fullname, setup.iso_info.release, setup.iso_info.isorelease
);
- let inner = LinearLayout::vertical()
- .child(PaddedView::lrtb(1, 1, 0, 1, TextView::new(logo).center()))
- .child(Dialog::around(view).title(title));
+ 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>);
+}
+
+struct InstallerBackgroundView {
+ view: StackView,
+}
+
+impl InstallerBackgroundView {
+ pub fn new() -> Self {
+ let style = Style {
+ effects: Effect::Bold.into(),
+ color: ColorStyle::back(PaletteColor::View),
+ };
+
+ 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 { view }
+ }
+}
+
+impl ViewWrapper for InstallerBackgroundView {
+ cursive::wrap_impl!(self.view: StackView);
}
#[derive(Clone, Eq, Hash, PartialEq)]
#[derive(Clone)]
struct InstallerState {
options: InstallerOptions,
- available_disks: Vec<Disk>,
setup_info: SetupInfo,
+ runtime_info: RuntimeInfo,
locales: LocaleInfo,
steps: HashMap<InstallerStep, ScreenId>,
in_test_mode: bool,
Some("-t") => true,
// Always force the test directory in debug builds
- #[cfg(debug_assertions)]
- _ => true,
-
- #[cfg(not(debug_assertions))]
- _ => false,
+ _ => cfg!(debug_assertions),
};
- let (setup_info, locales) = match installer_setup(in_test_mode) {
+ 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);
- // TODO: retrieve actual disk info
- let available_disks = vec![Disk {
- path: "/dev/vda".to_owned(),
- size: 17179869184,
- }];
-
siv.set_user_data(InstallerState {
options: InstallerOptions {
- bootdisk: BootdiskOptions::defaults_from(&available_disks[0]),
- timezone: TimezoneOptions::default(),
- password: PasswordOptions::default(),
- network: NetworkOptions::default(),
- reboot: false,
+ 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,
},
- available_disks,
setup_info,
+ runtime_info,
locales,
steps: HashMap::new(),
in_test_mode,
siv.run();
}
-fn installer_setup(in_test_mode: bool) -> Result<(SetupInfo, LocaleInfo), String> {
- system::has_min_requirements()?;
-
- let testdir = || {
- env::current_dir()
- .map(|mut p| {
- p.push("testdir");
- p
- })
- .map_err(|err| err.to_string())
- };
-
- let mut path = if in_test_mode {
- testdir()?
- } else {
- std::path::PathBuf::from("/")
- };
-
- 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}"))?
- };
+/// 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();
- let locale_info = {
- let mut path = path.clone();
- path.push("locales.json");
+ 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}"));
+ }
+ }
+ }
- setup::read_json(&path).map_err(|err| format!("Failed to retrieve locale info: {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."
+ ),
+ );
+ }
- Ok((installer_info, locale_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."
+ ),
+ );
+ }
}
fn initial_setup_error(siv: &mut CursiveRunnable, message: &str) -> ! {
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(state) = siv.user_data::<InstallerState>().cloned() {
- if let Some(screen_id) = state.steps.get(&step) {
- siv.set_screen(*screen_id);
- return;
+ 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);
}
-#[cfg(not(debug_assertions))]
-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(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.available_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)
- .flatten();
-
- if let Some(options) = options {
- siv.with_user_data(|state: &mut InstallerState| {
- state.options.bootdisk = options;
- });
-
- switch_to_next_screen(siv, InstallerStep::Timezone, &timezone_dialog);
- } else {
- siv.add_layer(Dialog::info("Invalid values"));
+ let options = siv.call_on_name("bootdisk-options", BootdiskOptionsView::get_values);
+
+ match options {
+ Some(Ok(options)) => {
+ siv.with_user_data(|state: &mut InstallerState| {
+ state.options.bootdisk = options;
+ });
+
+ switch_to_next_screen(siv, InstallerStep::Timezone, &timezone_dialog);
+ }
+
+ Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
+ _ => 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.ends_with(".invalid") {
+ } 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(vec!["eth0"]),
- )
+ .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();
+ .child(PaddedView::lrtb(1, 1, 1, 0, bbar));
- siv.with_user_data(|state: &mut InstallerState| {
- state.options.reboot = reboot;
- });
-
- 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 state = siv.user_data::<InstallerState>().unwrap();
- let progress_text = TextContent::new("extracting ..");
- let progress_bar = ProgressBar::new()
- .with_task({
- move |counter| {
- for _ in 0..100 {
- std::thread::sleep(std::time::Duration::from_millis(50));
- counter.tick(1);
- }
- }
- })
- .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()),
- );
-
- InstallerView::with_raw(state, inner)
+ let state = siv.user_data::<InstallerState>().cloned().unwrap();
+ InstallerView::with_raw(&state, InstallProgressView::new(siv))
}