1 #![forbid(unsafe_code)]
6 io
::{BufRead, BufReader, Write}
,
16 theme
::{ColorStyle, Effect, PaletteColor, Style}
,
18 view
::{Nameable, Offset, Resizable, ViewWrapper}
,
20 Button
, Checkbox
, Dialog
, DummyView
, EditView
, Layer
, LinearLayout
, PaddedView
, Panel
,
21 ProgressBar
, ResizedView
, ScrollView
, SelectView
, StackView
, TextContent
, TextView
,
24 Cursive
, CursiveRunnable
, ScreenId
, View
, XY
,
30 use options
::InstallerOptions
;
32 use proxmox_installer_common
::{
33 options
::{BootdiskOptions, NetworkOptions, PasswordOptions, TimezoneOptions}
,
34 setup
::{installer_setup, LocaleInfo, ProxmoxProduct, RuntimeInfo, SetupInfo}
,
39 use setup
::InstallConfig
;
45 BootdiskOptionsView
, CidrAddressEditView
, FormView
, TableView
, TableViewItem
,
49 // TextView::center() seems to garble the first two lines, so fix it manually here.
50 const PROXMOX_LOGO
: &str = r
#"
52 | _ \ _ __ _____ ___ __ ___ _____ __
53 | |_) | '__/ _ \ \/ / '_ ` _ \ / _ \ \/ /
54 | __/| | | (_) > <| | | | | | (_) > <
55 |_| |_| \___/_/\_\_| |_| |_|\___/_/\_\ "#;
57 struct InstallerView
{
58 view
: ResizedView
<Dialog
>,
63 state
: &InstallerState
,
65 next_cb
: Box
<dyn Fn(&mut Cursive
)>,
68 let mut bbar
= LinearLayout
::horizontal()
69 .child(abort_install_button())
70 .child(DummyView
.full_width())
71 .child(Button
::new("Previous", switch_to_prev_screen
))
73 .child(Button
::new("Next", next_cb
));
74 let _
= bbar
.set_focus_index(4); // ignore errors
75 let mut inner
= LinearLayout
::vertical()
76 .child(PaddedView
::lrtb(0, 0, 1, 1, view
))
77 .child(PaddedView
::lrtb(1, 1, 0, 0, bbar
));
79 let _
= inner
.set_focus_index(1); // ignore errors
82 Self::with_raw(state
, inner
)
85 pub fn with_raw(state
: &InstallerState
, view
: impl View
) -> Self {
86 let setup
= &state
.setup_info
;
89 "{} ({}-{}) Installer",
90 setup
.config
.fullname
, setup
.iso_info
.release
, setup
.iso_info
.isorelease
93 let inner
= Dialog
::around(view
).title(title
);
96 // Limit the maximum to something reasonable, such that it won't get spread out much
97 // depending on the screen.
98 view
: ResizedView
::with_max_size((120, 40), inner
),
103 impl ViewWrapper
for InstallerView
{
104 cursive
::wrap_impl
!(self.view
: ResizedView
<Dialog
>);
107 struct InstallerBackgroundView
{
111 impl InstallerBackgroundView
{
112 pub fn new() -> Self {
114 effects
: Effect
::Bold
.into(),
115 color
: ColorStyle
::back(PaletteColor
::View
),
118 let mut view
= StackView
::new();
119 view
.add_fullscreen_layer(Layer
::with_color(
122 .fixed_height(PROXMOX_LOGO
.lines().count() + 1),
123 ColorStyle
::back(PaletteColor
::View
),
125 view
.add_transparent_layer_at(
128 y
: Offset
::Absolute(0),
130 TextView
::new(PROXMOX_LOGO
).style(style
),
137 impl ViewWrapper
for InstallerBackgroundView
{
138 cursive
::wrap_impl
!(self.view
: StackView
);
141 #[derive(Clone, Eq, Hash, PartialEq)]
153 struct InstallerState
{
154 options
: InstallerOptions
,
155 setup_info
: SetupInfo
,
156 runtime_info
: RuntimeInfo
,
158 steps
: HashMap
<InstallerStep
, ScreenId
>,
163 let mut siv
= cursive
::termion();
165 let in_test_mode
= match env
::args().nth(1).as_deref() {
168 // Always force the test directory in debug builds
169 _
=> cfg
!(debug_assertions
),
172 let (setup_info
, locales
, runtime_info
) = match installer_setup(in_test_mode
) {
173 Ok(result
) => result
,
174 Err(err
) => initial_setup_error(&mut siv
, &err
),
177 siv
.clear_global_callbacks(Event
::CtrlChar('c'
));
178 siv
.set_on_pre_event(Event
::CtrlChar('c'
), trigger_abort_install_dialog
);
180 siv
.set_user_data(InstallerState
{
181 options
: InstallerOptions
{
182 bootdisk
: BootdiskOptions
::defaults_from(&runtime_info
.disks
[0]),
183 timezone
: TimezoneOptions
::defaults_from(&runtime_info
, &locales
),
184 password
: Default
::default(),
185 network
: NetworkOptions
::defaults_from(&setup_info
, &runtime_info
.network
),
191 steps
: HashMap
::new(),
195 switch_to_next_screen(&mut siv
, InstallerStep
::Licence
, &license_dialog
);
199 /// Anything that can be done late in the setup and will not result in fatal errors.
200 fn installer_setup_late(siv
: &mut Cursive
) {
201 let state
= siv
.user_data
::<InstallerState
>().cloned().unwrap();
203 if !state
.in_test_mode
{
204 let kmap_id
= &state
.options
.timezone
.kb_layout
;
205 if let Some(kmap
) = state
.locales
.kmap
.get(kmap_id
) {
206 if let Err(err
) = system
::set_keyboard_layout(kmap
) {
207 display_setup_warning(siv
, &format
!("Failed to apply keyboard layout: {err}"));
212 if state
.runtime_info
.total_memory
< 1024 {
213 display_setup_warning(
216 "Less than 1 GiB of usable memory detected, installation will probably fail.\n\n",
217 "See 'System Requirements' in the documentation."
222 if state
.setup_info
.config
.product
== ProxmoxProduct
::PVE
&& !state
.runtime_info
.hvm_supported
{
223 display_setup_warning(
226 "No support for hardware-accelerated KVM virtualization detected.\n\n",
227 "Check BIOS settings for Intel VT / AMD-V / SVM."
233 fn initial_setup_error(siv
: &mut CursiveRunnable
, message
: &str) -> ! {
235 Dialog
::around(TextView
::new(message
))
236 .title("Installer setup error")
237 .button("Ok", Cursive
::quit
),
241 std
::process
::exit(1);
244 fn display_setup_warning(siv
: &mut Cursive
, message
: &str) {
245 siv
.add_layer(Dialog
::info(message
).title("Warning"));
248 fn switch_to_next_screen(
251 constructor
: &dyn Fn(&mut Cursive
) -> InstallerView
,
253 let state
= siv
.user_data
::<InstallerState
>().cloned().unwrap();
254 let is_first_screen
= state
.steps
.is_empty();
256 // Check if the screen already exists; if yes, then simply switch to it.
257 if let Some(screen_id
) = state
.steps
.get(&step
) {
258 siv
.set_screen(*screen_id
);
260 // The summary view cannot be cached (otherwise it would display stale values). Thus
261 // replace it if the screen is switched to.
262 // TODO: Could be done by e.g. having all the main dialog views implement some sort of
263 // .refresh(), which can be called if the view is switched to.
264 if step
== InstallerStep
::Summary
{
265 let view
= constructor(siv
);
266 siv
.screen_mut().pop_layer();
267 siv
.screen_mut().add_layer(view
);
273 let v
= constructor(siv
);
274 let screen
= siv
.add_active_screen();
275 siv
.with_user_data(|state
: &mut InstallerState
| state
.steps
.insert(step
, screen
));
277 siv
.screen_mut().add_transparent_layer_at(
279 x
: Offset
::Parent(0),
280 y
: Offset
::Parent(0),
282 InstallerBackgroundView
::new(),
285 siv
.screen_mut().add_layer(v
);
287 // If this is the first screen to be added, execute our late setup first.
288 // Needs to be done here, at the end, to ensure that any potential layers get added to
289 // the right screen and are on top.
291 installer_setup_late(siv
);
295 fn switch_to_prev_screen(siv
: &mut Cursive
) {
296 let id
= siv
.active_screen().saturating_sub(1);
304 callback_yes
: Box
<dyn Fn(&mut Cursive
)>,
305 callback_no
: Box
<dyn Fn(&mut Cursive
)>,
308 Dialog
::around(TextView
::new(text
))
310 .button("No", move |siv
| {
314 .button("Yes", move |siv
| {
321 fn trigger_abort_install_dialog(siv
: &mut Cursive
) {
322 #[cfg(debug_assertions)]
325 #[cfg(not(debug_assertions))]
328 "Abort installation?",
329 "Are you sure you want to abort the installation?",
330 Box
::new(Cursive
::quit
),
335 fn abort_install_button() -> Button
{
336 Button
::new("Abort", trigger_abort_install_dialog
)
339 fn get_eula(setup
: &SetupInfo
) -> String
{
340 let mut path
= setup
.locations
.iso
.clone();
343 std
::fs
::read_to_string(path
)
344 .unwrap_or_else(|_
| "< Debug build - ignoring non-existing EULA >".to_owned())
347 fn license_dialog(siv
: &mut Cursive
) -> InstallerView
{
348 let state
= siv
.user_data
::<InstallerState
>().unwrap();
350 let mut bbar
= LinearLayout
::horizontal()
351 .child(abort_install_button())
352 .child(DummyView
.full_width())
353 .child(Button
::new("I agree", |siv
| {
354 switch_to_next_screen(siv
, InstallerStep
::Bootdisk
, &bootdisk_dialog
)
356 let _
= bbar
.set_focus_index(2); // ignore errors
358 let mut inner
= LinearLayout
::vertical()
359 .child(PaddedView
::lrtb(
364 TextView
::new("END USER LICENSE AGREEMENT (EULA)").center(),
366 .child(Panel
::new(ScrollView
::new(
367 TextView
::new(get_eula(&state
.setup_info
)).center(),
369 .child(PaddedView
::lrtb(1, 1, 1, 0, bbar
));
371 let _
= inner
.set_focus_index(2); // ignore errors
373 InstallerView
::with_raw(state
, inner
)
376 fn bootdisk_dialog(siv
: &mut Cursive
) -> InstallerView
{
377 let state
= siv
.user_data
::<InstallerState
>().cloned().unwrap();
381 BootdiskOptionsView
::new(siv
, &state
.runtime_info
.disks
, &state
.options
.bootdisk
)
382 .with_name("bootdisk-options"),
384 let options
= siv
.call_on_name("bootdisk-options", BootdiskOptionsView
::get_values
);
387 Some(Ok(options
)) => {
388 siv
.with_user_data(|state
: &mut InstallerState
| {
389 state
.options
.bootdisk
= options
;
392 switch_to_next_screen(siv
, InstallerStep
::Timezone
, &timezone_dialog
);
395 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
396 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
403 fn timezone_dialog(siv
: &mut Cursive
) -> InstallerView
{
404 let state
= siv
.user_data
::<InstallerState
>().unwrap();
405 let options
= &state
.options
.timezone
;
409 TimezoneOptionsView
::new(&state
.locales
, options
).with_name("timezone-options"),
411 let options
= siv
.call_on_name("timezone-options", TimezoneOptionsView
::get_values
);
414 Some(Ok(options
)) => {
415 siv
.with_user_data(|state
: &mut InstallerState
| {
416 state
.options
.timezone
= options
;
419 switch_to_next_screen(siv
, InstallerStep
::Password
, &password_dialog
);
421 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
422 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
429 fn password_dialog(siv
: &mut Cursive
) -> InstallerView
{
430 let state
= siv
.user_data
::<InstallerState
>().unwrap();
431 let options
= &state
.options
.password
;
433 let inner
= FormView
::new()
434 .child("Root password", EditView
::new().secret())
435 .child("Confirm root password", EditView
::new().secret())
437 "Administator email",
438 EditView
::new().content(&options
.email
),
440 .with_name("password-options");
446 let options
= siv
.call_on_name("password-options", |view
: &mut FormView
| {
447 let root_password
= view
448 .get_value
::<EditView
, _
>(0)
449 .ok_or("failed to retrieve password")?
;
451 let confirm_password
= view
452 .get_value
::<EditView
, _
>(1)
453 .ok_or("failed to retrieve password confirmation")?
;
456 .get_value
::<EditView
, _
>(2)
457 .ok_or("failed to retrieve email")?
;
460 Regex
::new(r
"^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$")
463 if root_password
.len() < 5 {
464 Err("password too short, must be at least 5 characters long")
465 } else if root_password
!= confirm_password
{
466 Err("passwords do not match")
467 } else if email
== "mail@example.invalid" {
468 Err("invalid email address")
469 } else if !email_regex
.is_match(&email
) {
470 Err("Email does not look like a valid address (user@domain.tld)")
480 Some(Ok(options
)) => {
481 siv
.with_user_data(|state
: &mut InstallerState
| {
482 state
.options
.password
= options
;
485 switch_to_next_screen(siv
, InstallerStep
::Network
, &network_dialog
);
487 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
488 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
495 fn network_dialog(siv
: &mut Cursive
) -> InstallerView
{
496 let state
= siv
.user_data
::<InstallerState
>().unwrap();
497 let options
= &state
.options
.network
;
498 let ifaces
= state
.runtime_info
.network
.interfaces
.values();
501 .map(|iface
| (iface
.render(), iface
.name
.clone()));
502 let mut ifaces_selection
= SelectView
::new().popup().with_all(ifnames
.clone());
504 ifaces_selection
.sort();
505 ifaces_selection
.set_selection(
508 .position(|iface
| iface
.1 == options
.ifname
)
509 .unwrap_or(ifaces
.len() - 1),
512 let inner
= FormView
::new()
513 .child("Management interface", ifaces_selection
)
516 EditView
::new().content(options
.fqdn
.to_string()),
520 CidrAddressEditView
::new().content(options
.address
.clone()),
524 EditView
::new().content(options
.gateway
.to_string()),
527 "DNS server address",
528 EditView
::new().content(options
.dns_server
.to_string()),
530 .with_name("network-options");
536 let options
= siv
.call_on_name("network-options", |view
: &mut FormView
| {
538 .get_value
::<SelectView
, _
>(0)
539 .ok_or("failed to retrieve management interface name")?
;
542 .get_value
::<EditView
, _
>(1)
543 .ok_or("failed to retrieve host FQDN")?
545 .map_err(|err
| format
!("hostname does not look valid:\n\n{err}"))?
;
548 .get_value
::<CidrAddressEditView
, _
>(2)
549 .ok_or("failed to retrieve host address")?
;
552 .get_value
::<EditView
, _
>(3)
553 .ok_or("failed to retrieve gateway address")?
555 .map_err(|err
| err
.to_string())?
;
557 let dns_server
= view
558 .get_value
::<EditView
, _
>(4)
559 .ok_or("failed to retrieve DNS server address")?
561 .map_err(|err
| err
.to_string())?
;
563 if address
.addr().is_ipv4() != gateway
.is_ipv4() {
564 Err("host and gateway IP address version must not differ".to_owned())
565 } else if address
.addr().is_ipv4() != dns_server
.is_ipv4() {
566 Err("host and DNS IP address version must not differ".to_owned())
567 } else if fqdn
.to_string().ends_with(".invalid") {
568 Err("hostname does not look valid".to_owned())
581 Some(Ok(options
)) => {
582 siv
.with_user_data(|state
: &mut InstallerState
| {
583 state
.options
.network
= options
;
586 switch_to_next_screen(siv
, InstallerStep
::Summary
, &summary_dialog
);
588 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
589 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
596 pub struct SummaryOption
{
602 pub fn new
<S
: Into
<String
>>(name
: &'
static str, value
: S
) -> Self {
610 impl TableViewItem
for SummaryOption
{
611 fn get_column(&self, name
: &str) -> String
{
613 "name" => self.name
.to_owned(),
614 "value" => self.value
.clone(),
620 fn summary_dialog(siv
: &mut Cursive
) -> InstallerView
{
621 let state
= siv
.user_data
::<InstallerState
>().unwrap();
623 let mut bbar
= LinearLayout
::horizontal()
624 .child(abort_install_button())
625 .child(DummyView
.full_width())
626 .child(Button
::new("Previous", switch_to_prev_screen
))
628 .child(Button
::new("Install", |siv
| {
630 .find_name("reboot-after-install")
631 .map(|v
: ViewRef
<Checkbox
>| v
.is_checked())
632 .unwrap_or_default();
634 siv
.with_user_data(|state
: &mut InstallerState
| {
635 state
.options
.autoreboot
= autoreboot
;
638 switch_to_next_screen(siv
, InstallerStep
::Install
, &install_progress_dialog
);
641 let _
= bbar
.set_focus_index(2); // ignore errors
643 let mut inner
= LinearLayout
::vertical()
644 .child(PaddedView
::lrtb(
651 ("name".to_owned(), "Option".to_owned()),
652 ("value".to_owned(), "Selected value".to_owned()),
654 .items(state
.options
.to_summary(&state
.locales
)),
657 LinearLayout
::horizontal()
658 .child(DummyView
.full_width())
659 .child(Checkbox
::new().checked().with_name("reboot-after-install"))
661 TextView
::new(" Automatically reboot after successful installation").no_wrap(),
663 .child(DummyView
.full_width()),
665 .child(PaddedView
::lrtb(1, 1, 1, 0, bbar
));
667 let _
= inner
.set_focus_index(2); // ignore errors
669 InstallerView
::with_raw(state
, inner
)
672 fn install_progress_dialog(siv
: &mut Cursive
) -> InstallerView
{
673 // Ensure the screen is updated independently of keyboard events and such
674 siv
.set_autorefresh(true);
676 let cb_sink
= siv
.cb_sink().clone();
677 let state
= siv
.user_data
::<InstallerState
>().unwrap();
678 let in_test_mode
= state
.in_test_mode
;
679 let progress_text
= TextContent
::new("starting the installation ..");
681 let progress_task
= {
682 let progress_text
= progress_text
.clone();
683 let options
= state
.options
.clone();
684 move |counter
: Counter
| {
686 use std
::process
::{Command, Stdio}
;
688 let (path
, args
, envs
): (&str, &[&str], Vec
<(&str, &str)>) = if in_test_mode
{
690 "./proxmox-low-level-installer",
691 &["-t", "start-session-test"],
692 vec
![("PERL5LIB", ".")],
695 ("proxmox-low-level-installer", &["start-session"], vec
![])
701 .stdin(Stdio
::piped())
702 .stdout(Stdio
::piped())
706 let mut child
= match child
{
709 let _
= cb_sink
.send(Box
::new(move |siv
| {
711 Dialog
::text(err
.to_string())
713 .button("Ok", Cursive
::quit
),
721 let reader
= child
.stdout
.take().map(BufReader
::new
)?
;
722 let mut writer
= child
.stdin
.take()?
;
724 serde_json
::to_writer(&mut writer
, &InstallConfig
::from(options
)).unwrap();
725 writeln
!(writer
).unwrap();
727 let writer
= Arc
::new(Mutex
::new(writer
));
729 for line
in reader
.lines() {
730 let line
= match line
{
735 let msg
= match line
.parse
::<UiMessage
>() {
738 eprintln
!("low-level installer: {stray}");
744 UiMessage
::Info(s
) => cb_sink
.send(Box
::new(|siv
| {
745 siv
.add_layer(Dialog
::info(s
).title("Information"));
747 UiMessage
::Error(s
) => cb_sink
.send(Box
::new(|siv
| {
748 siv
.add_layer(Dialog
::info(s
).title("Error"));
750 UiMessage
::Prompt(s
) => cb_sink
.send({
751 let writer
= writer
.clone();
752 Box
::new(move |siv
| {
758 let writer
= writer
.clone();
760 if let Ok(mut writer
) = writer
.lock() {
761 let _
= writeln
!(writer
, "ok");
766 if let Ok(mut writer
) = writer
.lock() {
767 let _
= writeln
!(writer
);
773 UiMessage
::Progress(ratio
, s
) => {
775 progress_text
.set_content(s
);
778 UiMessage
::Finished(success
, msg
) => {
780 progress_text
.set_content(msg
.to_owned());
781 cb_sink
.send(Box
::new(move |siv
| {
782 let title
= if success { "Success" }
else { "Failure" }
;
784 // For rebooting, we just need to quit the installer,
785 // our caller does the actual reboot.
789 .button("Reboot now", Cursive
::quit
),
793 .user_data
::<InstallerState
>()
794 .map(|state
| state
.options
.autoreboot
)
795 .unwrap_or_default();
797 if autoreboot
&& success
{
798 let cb_sink
= siv
.cb_sink();
800 let cb_sink
= cb_sink
.clone();
802 thread
::sleep(Duration
::from_secs(5));
803 let _
= cb_sink
.send(Box
::new(Cursive
::quit
));
816 if inner().is_none() {
818 .send(Box
::new(|siv
| {
820 Dialog
::text("low-level installer exited early")
822 .button("Exit", Cursive
::quit
),
830 let progress_bar
= ProgressBar
::new().with_task(progress_task
).full_width();
831 let inner
= PaddedView
::lrtb(
836 LinearLayout
::vertical()
837 .child(PaddedView
::lrtb(1, 1, 0, 0, progress_bar
))
839 .child(TextView
::new_with_content(progress_text
).center())
840 .child(PaddedView
::lrtb(
845 LinearLayout
::horizontal().child(abort_install_button()),
849 InstallerView
::with_raw(state
, inner
)
856 Finished(bool
, String
),
857 Progress(usize, String
),
860 impl FromStr
for UiMessage
{
863 fn from_str(s
: &str) -> Result
<Self, Self::Err
> {
864 let (ty
, rest
) = s
.split_once(": ").ok_or("invalid message: no type")?
;
867 "message" => Ok(UiMessage
::Info(rest
.to_owned())),
868 "error" => Ok(UiMessage
::Error(rest
.to_owned())),
869 "prompt" => Ok(UiMessage
::Prompt(rest
.to_owned())),
871 let (state
, rest
) = rest
.split_once(", ").ok_or("invalid message: no state")?
;
872 Ok(UiMessage
::Finished(state
== "ok", rest
.to_owned()))
875 let (percent
, rest
) = rest
.split_once(' '
).ok_or("invalid progress message")?
;
876 Ok(UiMessage
::Progress(
879 .map(|v
| (v
* 100.).floor() as usize)
880 .map_err(|err
| err
.to_string())?
,
884 unknown
=> Err(format
!("invalid message type {unknown}, rest: {rest}")),