1 #![forbid(unsafe_code)]
3 use std
::{collections::HashMap, env, net::IpAddr}
;
7 theme
::{ColorStyle, Effect, PaletteColor, Style}
,
8 view
::{Nameable, Offset, Resizable, ViewWrapper}
,
10 Button
, Checkbox
, Dialog
, DummyView
, EditView
, Layer
, LinearLayout
, PaddedView
, Panel
,
11 ResizedView
, ScrollView
, SelectView
, StackView
, TextView
, ViewRef
,
13 Cursive
, CursiveRunnable
, ScreenId
, View
, XY
,
19 use options
::InstallerOptions
;
21 use proxmox_installer_common
::{
22 options
::{BootdiskOptions, NetworkOptions, PasswordOptions, TimezoneOptions}
,
23 setup
::{installer_setup, LocaleInfo, ProxmoxProduct, RuntimeInfo, SetupInfo}
,
33 BootdiskOptionsView
, CidrAddressEditView
, FormView
, InstallProgressView
, TableView
,
34 TableViewItem
, TimezoneOptionsView
,
37 // TextView::center() seems to garble the first two lines, so fix it manually here.
38 const PROXMOX_LOGO
: &str = r
#"
40 | _ \ _ __ _____ ___ __ ___ _____ __
41 | |_) | '__/ _ \ \/ / '_ ` _ \ / _ \ \/ /
42 | __/| | | (_) > <| | | | | | (_) > <
43 |_| |_| \___/_/\_\_| |_| |_|\___/_/\_\ "#;
45 struct InstallerView
{
46 view
: ResizedView
<Dialog
>,
51 state
: &InstallerState
,
53 next_cb
: Box
<dyn Fn(&mut Cursive
)>,
56 let mut bbar
= LinearLayout
::horizontal()
57 .child(abort_install_button())
58 .child(DummyView
.full_width())
59 .child(Button
::new("Previous", switch_to_prev_screen
))
61 .child(Button
::new("Next", next_cb
));
62 let _
= bbar
.set_focus_index(4); // ignore errors
63 let mut inner
= LinearLayout
::vertical()
64 .child(PaddedView
::lrtb(0, 0, 1, 1, view
))
65 .child(PaddedView
::lrtb(1, 1, 0, 0, bbar
));
67 let _
= inner
.set_focus_index(1); // ignore errors
70 Self::with_raw(state
, inner
)
73 pub fn with_raw(state
: &InstallerState
, view
: impl View
) -> Self {
74 let setup
= &state
.setup_info
;
77 "{} ({}-{}) Installer",
78 setup
.config
.fullname
, setup
.iso_info
.release
, setup
.iso_info
.isorelease
81 let inner
= Dialog
::around(view
).title(title
);
84 // Limit the maximum to something reasonable, such that it won't get spread out much
85 // depending on the screen.
86 view
: ResizedView
::with_max_size((120, 40), inner
),
91 impl ViewWrapper
for InstallerView
{
92 cursive
::wrap_impl
!(self.view
: ResizedView
<Dialog
>);
95 struct InstallerBackgroundView
{
99 impl InstallerBackgroundView
{
100 pub fn new() -> Self {
102 effects
: Effect
::Bold
.into(),
103 color
: ColorStyle
::back(PaletteColor
::View
),
106 let mut view
= StackView
::new();
107 view
.add_fullscreen_layer(Layer
::with_color(
110 .fixed_height(PROXMOX_LOGO
.lines().count() + 1),
111 ColorStyle
::back(PaletteColor
::View
),
113 view
.add_transparent_layer_at(
116 y
: Offset
::Absolute(0),
118 TextView
::new(PROXMOX_LOGO
).style(style
),
125 impl ViewWrapper
for InstallerBackgroundView
{
126 cursive
::wrap_impl
!(self.view
: StackView
);
129 #[derive(Clone, Eq, Hash, PartialEq)]
141 struct InstallerState
{
142 options
: InstallerOptions
,
143 setup_info
: SetupInfo
,
144 runtime_info
: RuntimeInfo
,
146 steps
: HashMap
<InstallerStep
, ScreenId
>,
151 let mut siv
= cursive
::termion();
153 let in_test_mode
= match env
::args().nth(1).as_deref() {
156 // Always force the test directory in debug builds
157 _
=> cfg
!(debug_assertions
),
160 let (setup_info
, locales
, runtime_info
) = match installer_setup(in_test_mode
) {
161 Ok(result
) => result
,
162 Err(err
) => initial_setup_error(&mut siv
, &err
),
165 siv
.clear_global_callbacks(Event
::CtrlChar('c'
));
166 siv
.set_on_pre_event(Event
::CtrlChar('c'
), trigger_abort_install_dialog
);
168 siv
.set_user_data(InstallerState
{
169 options
: InstallerOptions
{
170 bootdisk
: BootdiskOptions
::defaults_from(&runtime_info
.disks
[0]),
171 timezone
: TimezoneOptions
::defaults_from(&runtime_info
, &locales
),
172 password
: Default
::default(),
173 network
: NetworkOptions
::defaults_from(&setup_info
, &runtime_info
.network
),
179 steps
: HashMap
::new(),
183 switch_to_next_screen(&mut siv
, InstallerStep
::Licence
, &license_dialog
);
187 /// Anything that can be done late in the setup and will not result in fatal errors.
188 fn installer_setup_late(siv
: &mut Cursive
) {
189 let state
= siv
.user_data
::<InstallerState
>().cloned().unwrap();
191 if !state
.in_test_mode
{
192 let kmap_id
= &state
.options
.timezone
.kb_layout
;
193 if let Some(kmap
) = state
.locales
.kmap
.get(kmap_id
) {
194 if let Err(err
) = system
::set_keyboard_layout(kmap
) {
195 display_setup_warning(siv
, &format
!("Failed to apply keyboard layout: {err}"));
200 if state
.runtime_info
.total_memory
< 1024 {
201 display_setup_warning(
204 "Less than 1 GiB of usable memory detected, installation will probably fail.\n\n",
205 "See 'System Requirements' in the documentation."
210 if state
.setup_info
.config
.product
== ProxmoxProduct
::PVE
&& !state
.runtime_info
.hvm_supported
{
211 display_setup_warning(
214 "No support for hardware-accelerated KVM virtualization detected.\n\n",
215 "Check BIOS settings for Intel VT / AMD-V / SVM."
221 fn initial_setup_error(siv
: &mut CursiveRunnable
, message
: &str) -> ! {
223 Dialog
::around(TextView
::new(message
))
224 .title("Installer setup error")
225 .button("Ok", Cursive
::quit
),
229 std
::process
::exit(1);
232 fn display_setup_warning(siv
: &mut Cursive
, message
: &str) {
233 siv
.add_layer(Dialog
::info(message
).title("Warning"));
236 fn switch_to_next_screen(
239 constructor
: &dyn Fn(&mut Cursive
) -> InstallerView
,
241 let state
= siv
.user_data
::<InstallerState
>().cloned().unwrap();
242 let is_first_screen
= state
.steps
.is_empty();
244 // Check if the screen already exists; if yes, then simply switch to it.
245 if let Some(screen_id
) = state
.steps
.get(&step
) {
246 siv
.set_screen(*screen_id
);
248 // The summary view cannot be cached (otherwise it would display stale values). Thus
249 // replace it if the screen is switched to.
250 // TODO: Could be done by e.g. having all the main dialog views implement some sort of
251 // .refresh(), which can be called if the view is switched to.
252 if step
== InstallerStep
::Summary
{
253 let view
= constructor(siv
);
254 siv
.screen_mut().pop_layer();
255 siv
.screen_mut().add_layer(view
);
261 let v
= constructor(siv
);
262 let screen
= siv
.add_active_screen();
263 siv
.with_user_data(|state
: &mut InstallerState
| state
.steps
.insert(step
, screen
));
265 siv
.screen_mut().add_transparent_layer_at(
267 x
: Offset
::Parent(0),
268 y
: Offset
::Parent(0),
270 InstallerBackgroundView
::new(),
273 siv
.screen_mut().add_layer(v
);
275 // If this is the first screen to be added, execute our late setup first.
276 // Needs to be done here, at the end, to ensure that any potential layers get added to
277 // the right screen and are on top.
279 installer_setup_late(siv
);
283 fn switch_to_prev_screen(siv
: &mut Cursive
) {
284 let id
= siv
.active_screen().saturating_sub(1);
293 callback_yes
: Box
<dyn Fn(&mut Cursive
)>,
295 callback_no
: Box
<dyn Fn(&mut Cursive
)>,
298 Dialog
::around(TextView
::new(text
))
300 .button(no_text
, move |siv
| {
304 .button(yes_text
, move |siv
| {
311 fn trigger_abort_install_dialog(siv
: &mut Cursive
) {
312 #[cfg(debug_assertions)]
315 #[cfg(not(debug_assertions))]
318 "Abort installation?",
319 "Are you sure you want to abort the installation?",
321 Box
::new(Cursive
::quit
),
327 fn abort_install_button() -> Button
{
328 Button
::new("Abort", trigger_abort_install_dialog
)
331 fn get_eula(setup
: &SetupInfo
) -> String
{
332 let mut path
= setup
.locations
.iso
.clone();
335 std
::fs
::read_to_string(path
)
336 .unwrap_or_else(|_
| "< Debug build - ignoring non-existing EULA >".to_owned())
339 fn license_dialog(siv
: &mut Cursive
) -> InstallerView
{
340 let state
= siv
.user_data
::<InstallerState
>().unwrap();
342 let mut bbar
= LinearLayout
::horizontal()
343 .child(abort_install_button())
344 .child(DummyView
.full_width())
345 .child(Button
::new("I agree", |siv
| {
346 switch_to_next_screen(siv
, InstallerStep
::Bootdisk
, &bootdisk_dialog
)
348 let _
= bbar
.set_focus_index(2); // ignore errors
350 let mut inner
= LinearLayout
::vertical()
351 .child(PaddedView
::lrtb(
356 TextView
::new("END USER LICENSE AGREEMENT (EULA)").center(),
358 .child(Panel
::new(ScrollView
::new(TextView
::new(get_eula(
361 .child(PaddedView
::lrtb(1, 1, 1, 0, bbar
));
363 let _
= inner
.set_focus_index(2); // ignore errors
365 InstallerView
::with_raw(state
, inner
)
368 fn bootdisk_dialog(siv
: &mut Cursive
) -> InstallerView
{
369 let state
= siv
.user_data
::<InstallerState
>().cloned().unwrap();
373 BootdiskOptionsView
::new(siv
, &state
.runtime_info
, &state
.options
.bootdisk
)
374 .with_name("bootdisk-options"),
376 let options
= siv
.call_on_name("bootdisk-options", BootdiskOptionsView
::get_values
);
379 Some(Ok(options
)) => {
380 siv
.with_user_data(|state
: &mut InstallerState
| {
381 state
.options
.bootdisk
= options
;
384 switch_to_next_screen(siv
, InstallerStep
::Timezone
, &timezone_dialog
);
387 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
388 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
395 fn timezone_dialog(siv
: &mut Cursive
) -> InstallerView
{
396 let state
= siv
.user_data
::<InstallerState
>().unwrap();
397 let options
= &state
.options
.timezone
;
401 TimezoneOptionsView
::new(&state
.locales
, options
).with_name("timezone-options"),
403 let options
= siv
.call_on_name("timezone-options", TimezoneOptionsView
::get_values
);
406 Some(Ok(options
)) => {
407 siv
.with_user_data(|state
: &mut InstallerState
| {
408 state
.options
.timezone
= options
;
411 switch_to_next_screen(siv
, InstallerStep
::Password
, &password_dialog
);
413 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
414 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
421 fn password_dialog(siv
: &mut Cursive
) -> InstallerView
{
422 let state
= siv
.user_data
::<InstallerState
>().unwrap();
423 let options
= &state
.options
.password
;
425 let inner
= FormView
::new()
426 .child("Root password", EditView
::new().secret())
427 .child("Confirm root password", EditView
::new().secret())
429 "Administrator email",
430 EditView
::new().content(&options
.email
),
432 .with_name("password-options");
438 let options
= siv
.call_on_name("password-options", |view
: &mut FormView
| {
439 let root_password
= view
440 .get_value
::<EditView
, _
>(0)
441 .ok_or("failed to retrieve password")?
;
443 let confirm_password
= view
444 .get_value
::<EditView
, _
>(1)
445 .ok_or("failed to retrieve password confirmation")?
;
448 .get_value
::<EditView
, _
>(2)
449 .ok_or("failed to retrieve email")?
;
452 Regex
::new(r
"^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$")
455 if root_password
.len() < 5 {
456 Err("password too short, must be at least 5 characters long")
457 } else if root_password
!= confirm_password
{
458 Err("passwords do not match")
459 } else if email
== "mail@example.invalid" {
460 Err("invalid email address")
461 } else if !email_regex
.is_match(&email
) {
462 Err("Email does not look like a valid address (user@domain.tld)")
472 Some(Ok(options
)) => {
473 siv
.with_user_data(|state
: &mut InstallerState
| {
474 state
.options
.password
= options
;
477 switch_to_next_screen(siv
, InstallerStep
::Network
, &network_dialog
);
479 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
480 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
487 fn network_dialog(siv
: &mut Cursive
) -> InstallerView
{
488 let state
= siv
.user_data
::<InstallerState
>().unwrap();
489 let options
= &state
.options
.network
;
490 let ifaces
= state
.runtime_info
.network
.interfaces
.values();
493 .map(|iface
| (iface
.render(), iface
.name
.clone()));
494 let mut ifaces_selection
= SelectView
::new().popup().with_all(ifnames
.clone());
496 // sort first to always have stable view
497 ifaces_selection
.sort();
498 let selected
= ifaces_selection
500 .position(|(_label
, iface
)| *iface
== options
.ifname
)
501 .unwrap_or(ifaces
.len() - 1);
503 ifaces_selection
.set_selection(selected
);
505 let inner
= FormView
::new()
506 .child("Management interface", ifaces_selection
)
509 EditView
::new().content(options
.fqdn
.to_string()),
513 CidrAddressEditView
::new().content(options
.address
.clone()),
517 EditView
::new().content(options
.gateway
.to_string()),
520 "DNS server address",
521 EditView
::new().content(options
.dns_server
.to_string()),
523 .with_name("network-options");
529 let options
= siv
.call_on_name("network-options", |view
: &mut FormView
| {
531 .get_value
::<SelectView
, _
>(0)
532 .ok_or("failed to retrieve management interface name")?
;
535 .get_value
::<EditView
, _
>(1)
536 .ok_or("failed to retrieve host FQDN")?
538 .map_err(|err
| format
!("hostname does not look valid:\n\n{err}"))?
;
541 .get_value
::<CidrAddressEditView
, _
>(2)
542 .ok_or("failed to retrieve host address")?
;
545 .get_value
::<EditView
, _
>(3)
546 .ok_or("failed to retrieve gateway address")?
548 .map_err(|err
| err
.to_string())?
;
550 let dns_server
= view
551 .get_value
::<EditView
, _
>(4)
552 .ok_or("failed to retrieve DNS server address")?
554 .map_err(|err
| err
.to_string())?
;
556 if address
.addr().is_ipv4() != gateway
.is_ipv4() {
557 Err("host and gateway IP address version must not differ".to_owned())
558 } else if address
.addr().is_ipv4() != dns_server
.is_ipv4() {
559 Err("host and DNS IP address version must not differ".to_owned())
560 } else if fqdn
.to_string().ends_with(".invalid") {
561 Err("hostname does not look valid".to_owned())
574 Some(Ok(options
)) => {
575 siv
.with_user_data(|state
: &mut InstallerState
| {
576 state
.options
.network
= options
;
579 switch_to_next_screen(siv
, InstallerStep
::Summary
, &summary_dialog
);
581 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
582 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
589 pub struct SummaryOption
{
595 pub fn new
<S
: Into
<String
>>(name
: &'
static str, value
: S
) -> Self {
603 impl TableViewItem
for SummaryOption
{
604 fn get_column(&self, name
: &str) -> String
{
606 "name" => self.name
.to_owned(),
607 "value" => self.value
.clone(),
613 fn summary_dialog(siv
: &mut Cursive
) -> InstallerView
{
614 let state
= siv
.user_data
::<InstallerState
>().unwrap();
616 let mut bbar
= LinearLayout
::horizontal()
617 .child(abort_install_button())
618 .child(DummyView
.full_width())
619 .child(Button
::new("Previous", switch_to_prev_screen
))
621 .child(Button
::new("Install", |siv
| {
623 .find_name("reboot-after-install")
624 .map(|v
: ViewRef
<Checkbox
>| v
.is_checked())
625 .unwrap_or_default();
627 siv
.with_user_data(|state
: &mut InstallerState
| {
628 state
.options
.autoreboot
= autoreboot
;
631 switch_to_next_screen(siv
, InstallerStep
::Install
, &install_progress_dialog
);
634 let _
= bbar
.set_focus_index(2); // ignore errors
636 let mut inner
= LinearLayout
::vertical()
637 .child(PaddedView
::lrtb(
644 ("name".to_owned(), "Option".to_owned()),
645 ("value".to_owned(), "Selected value".to_owned()),
647 .items(state
.options
.to_summary(&state
.locales
)),
650 LinearLayout
::horizontal()
651 .child(DummyView
.full_width())
652 .child(Checkbox
::new().checked().with_name("reboot-after-install"))
654 TextView
::new(" Automatically reboot after successful installation").no_wrap(),
656 .child(DummyView
.full_width()),
658 .child(PaddedView
::lrtb(1, 1, 1, 0, bbar
));
660 let _
= inner
.set_focus_index(2); // ignore errors
662 InstallerView
::with_raw(state
, inner
)
665 fn install_progress_dialog(siv
: &mut Cursive
) -> InstallerView
{
666 // Ensure the screen is updated independently of keyboard events and such
667 siv
.set_autorefresh(true);
669 let state
= siv
.user_data
::<InstallerState
>().cloned().unwrap();
670 InstallerView
::with_raw(&state
, InstallProgressView
::new(siv
))