4 io
::{BufRead, BufReader, Write}
,
15 theme
::{ColorStyle, Effect, PaletteColor, Style}
,
17 view
::{Nameable, Offset, Resizable, ViewWrapper}
,
19 Button
, Checkbox
, Dialog
, DummyView
, EditView
, Layer
, LinearLayout
, PaddedView
, Panel
,
20 ProgressBar
, ResizedView
, ScrollView
, SelectView
, StackView
, TextContent
, TextView
,
23 Cursive
, CursiveRunnable
, ScreenId
, View
, XY
,
32 use setup
::{InstallConfig, LocaleInfo, ProxmoxProduct, RuntimeInfo, SetupInfo}
;
41 BootdiskOptionsView
, CidrAddressEditView
, FormView
, TableView
, TableViewItem
,
45 // TextView::center() seems to garble the first two lines, so fix it manually here.
46 const PROXMOX_LOGO
: &str = r
#"
48 | _ \ _ __ _____ ___ __ ___ _____ __
49 | |_) | '__/ _ \ \/ / '_ ` _ \ / _ \ \/ /
50 | __/| | | (_) > <| | | | | | (_) > <
51 |_| |_| \___/_/\_\_| |_| |_|\___/_/\_\ "#;
53 /// ISO information is available globally.
54 static mut SETUP_INFO
: Option
<SetupInfo
> = None
;
56 pub fn setup_info() -> &'
static SetupInfo
{
57 unsafe { SETUP_INFO.as_ref().unwrap() }
60 fn init_setup_info(info
: SetupInfo
) {
62 SETUP_INFO
= Some(info
);
67 pub fn current_product() -> setup
::ProxmoxProduct
{
68 setup_info().config
.product
71 struct InstallerView
{
72 view
: ResizedView
<Dialog
>,
77 state
: &InstallerState
,
79 next_cb
: Box
<dyn Fn(&mut Cursive
)>,
82 let mut bbar
= LinearLayout
::horizontal()
83 .child(abort_install_button())
84 .child(DummyView
.full_width())
85 .child(Button
::new("Previous", switch_to_prev_screen
))
87 .child(Button
::new("Next", next_cb
));
88 let _
= bbar
.set_focus_index(4); // ignore errors
89 let mut inner
= LinearLayout
::vertical()
90 .child(PaddedView
::lrtb(0, 0, 1, 1, view
))
91 .child(PaddedView
::lrtb(1, 1, 0, 0, bbar
));
93 let _
= inner
.set_focus_index(1); // ignore errors
96 Self::with_raw(state
, inner
)
99 pub fn with_raw(state
: &InstallerState
, view
: impl View
) -> Self {
100 let setup
= &state
.setup_info
;
103 "{} ({}-{}) Installer",
104 setup
.config
.fullname
, setup
.iso_info
.release
, setup
.iso_info
.isorelease
107 let inner
= Dialog
::around(view
).title(title
);
110 // Limit the maximum to something reasonable, such that it won't get spread out much
111 // depending on the screen.
112 view
: ResizedView
::with_max_size((120, 40), inner
),
117 impl ViewWrapper
for InstallerView
{
118 cursive
::wrap_impl
!(self.view
: ResizedView
<Dialog
>);
121 struct InstallerBackgroundView
{
125 impl InstallerBackgroundView
{
126 pub fn new() -> Self {
128 effects
: Effect
::Bold
.into(),
129 color
: ColorStyle
::back(PaletteColor
::View
),
132 let mut view
= StackView
::new();
133 view
.add_fullscreen_layer(Layer
::with_color(
136 .fixed_height(PROXMOX_LOGO
.lines().count() + 1),
137 ColorStyle
::back(PaletteColor
::View
),
139 view
.add_transparent_layer_at(
142 y
: Offset
::Absolute(0),
144 TextView
::new(PROXMOX_LOGO
).style(style
),
151 impl ViewWrapper
for InstallerBackgroundView
{
152 cursive
::wrap_impl
!(self.view
: StackView
);
155 #[derive(Clone, Eq, Hash, PartialEq)]
167 struct InstallerState
{
168 options
: InstallerOptions
,
170 setup_info
: SetupInfo
,
171 runtime_info
: RuntimeInfo
,
173 steps
: HashMap
<InstallerStep
, ScreenId
>,
178 let mut siv
= cursive
::termion();
180 let in_test_mode
= match env
::args().nth(1).as_deref() {
183 // Always force the test directory in debug builds
184 _
=> cfg
!(debug_assertions
),
187 let (locales
, runtime_info
) = match installer_setup(in_test_mode
) {
188 Ok(result
) => result
,
189 Err(err
) => initial_setup_error(&mut siv
, &err
),
192 siv
.clear_global_callbacks(Event
::CtrlChar('c'
));
193 siv
.set_on_pre_event(Event
::CtrlChar('c'
), trigger_abort_install_dialog
);
195 siv
.set_user_data(InstallerState
{
196 options
: InstallerOptions
{
197 bootdisk
: BootdiskOptions
::defaults_from(&runtime_info
.disks
[0]),
198 timezone
: TimezoneOptions
::defaults_from(&runtime_info
, &locales
),
199 password
: Default
::default(),
200 network
: NetworkOptions
::from(&runtime_info
.network
),
203 setup_info
: setup_info().clone(), // FIXME: REMOVE
206 steps
: HashMap
::new(),
210 switch_to_next_screen(&mut siv
, InstallerStep
::Licence
, &license_dialog
);
214 fn installer_setup(in_test_mode
: bool
) -> Result
<(LocaleInfo
, RuntimeInfo
), String
> {
215 let base_path
= if in_test_mode { "./testdir" }
else { "/" }
;
216 let mut path
= PathBuf
::from(base_path
);
219 path
.push("proxmox-installer");
221 let installer_info
= {
222 let mut path
= path
.clone();
223 path
.push("iso-info.json");
225 setup
::read_json(&path
).map_err(|err
| format
!("Failed to retrieve setup info: {err}"))?
227 init_setup_info(installer_info
);
230 let mut path
= path
.clone();
231 path
.push("locales.json");
233 setup
::read_json(&path
).map_err(|err
| format
!("Failed to retrieve locale info: {err}"))?
236 let mut runtime_info
: RuntimeInfo
= {
237 let mut path
= path
.clone();
238 path
.push("run-env-info.json");
240 setup
::read_json(&path
)
241 .map_err(|err
| format
!("Failed to retrieve runtime environment info: {err}"))?
244 runtime_info
.disks
.sort();
245 if runtime_info
.disks
.is_empty() {
246 Err("The installer could not find any supported hard disks.".to_owned())
248 Ok((locale_info
, runtime_info
))
252 /// Anything that can be done late in the setup and will not result in fatal errors.
253 fn installer_setup_late(siv
: &mut Cursive
) {
254 let state
= siv
.user_data
::<InstallerState
>().cloned().unwrap();
256 if !state
.in_test_mode
{
257 let kmap_id
= &state
.options
.timezone
.kb_layout
;
258 if let Some(kmap
) = state
.locales
.kmap
.get(kmap_id
) {
259 if let Err(err
) = system
::set_keyboard_layout(kmap
) {
260 display_setup_warning(siv
, &format
!("Failed to apply keyboard layout: {err}"));
265 if state
.runtime_info
.total_memory
< 1024 {
266 display_setup_warning(
269 "Less than 1 GiB of usable memory detected, installation will probably fail.\n\n",
270 "See 'System Requirements' in the documentation."
275 if state
.setup_info
.config
.product
== ProxmoxProduct
::PVE
&& !state
.runtime_info
.hvm_supported
{
276 display_setup_warning(
279 "No support for hardware-accelerated KVM virtualization detected.\n\n",
280 "Check BIOS settings for Intel VT / AMD-V / SVM."
286 fn initial_setup_error(siv
: &mut CursiveRunnable
, message
: &str) -> ! {
288 Dialog
::around(TextView
::new(message
))
289 .title("Installer setup error")
290 .button("Ok", Cursive
::quit
),
294 std
::process
::exit(1);
297 fn display_setup_warning(siv
: &mut Cursive
, message
: &str) {
298 siv
.add_layer(Dialog
::info(message
).title("Warning"));
301 fn switch_to_next_screen(
304 constructor
: &dyn Fn(&mut Cursive
) -> InstallerView
,
306 let state
= siv
.user_data
::<InstallerState
>().cloned().unwrap();
307 let is_first_screen
= state
.steps
.is_empty();
309 // Check if the screen already exists; if yes, then simply switch to it.
310 if let Some(screen_id
) = state
.steps
.get(&step
) {
311 siv
.set_screen(*screen_id
);
313 // The summary view cannot be cached (otherwise it would display stale values). Thus
314 // replace it if the screen is switched to.
315 // TODO: Could be done by e.g. having all the main dialog views implement some sort of
316 // .refresh(), which can be called if the view is switched to.
317 if step
== InstallerStep
::Summary
{
318 let view
= constructor(siv
);
319 siv
.screen_mut().pop_layer();
320 siv
.screen_mut().add_layer(view
);
326 let v
= constructor(siv
);
327 let screen
= siv
.add_active_screen();
328 siv
.with_user_data(|state
: &mut InstallerState
| state
.steps
.insert(step
, screen
));
330 siv
.screen_mut().add_transparent_layer_at(
332 x
: Offset
::Parent(0),
333 y
: Offset
::Parent(0),
335 InstallerBackgroundView
::new(),
338 siv
.screen_mut().add_layer(v
);
340 // If this is the first screen to be added, execute our late setup first.
341 // Needs to be done here, at the end, to ensure that any potential layers get added to
342 // the right screen and are on top.
344 installer_setup_late(siv
);
348 fn switch_to_prev_screen(siv
: &mut Cursive
) {
349 let id
= siv
.active_screen().saturating_sub(1);
357 // callback_yes: &'static dyn Fn(&mut Cursive),
358 // callback_no: &'static dyn Fn(&mut Cursive),
359 callback_yes
: Box
<dyn Fn(&mut Cursive
)>,
360 callback_no
: Box
<dyn Fn(&mut Cursive
)>,
363 Dialog
::around(TextView
::new(text
))
365 .button("No", move |siv
| {
369 .button("Yes", move |siv
| {
376 fn trigger_abort_install_dialog(siv
: &mut Cursive
) {
377 #[cfg(debug_assertions)]
380 #[cfg(not(debug_assertions))]
383 "Abort installation?",
384 "Are you sure you want to abort the installation?",
385 Box
::new(Cursive
::quit
),
390 fn abort_install_button() -> Button
{
391 Button
::new("Abort", trigger_abort_install_dialog
)
394 fn get_eula() -> String
{
395 // TODO: properly using info from Proxmox::Install::Env::setup()
396 std
::fs
::read_to_string("/cdrom/EULA")
397 .unwrap_or_else(|_
| "< Debug build - ignoring non-existing EULA >".to_owned())
400 fn license_dialog(siv
: &mut Cursive
) -> InstallerView
{
401 let state
= siv
.user_data
::<InstallerState
>().unwrap();
403 let mut bbar
= LinearLayout
::horizontal()
404 .child(abort_install_button())
405 .child(DummyView
.full_width())
406 .child(Button
::new("I agree", |siv
| {
407 switch_to_next_screen(siv
, InstallerStep
::Bootdisk
, &bootdisk_dialog
)
409 let _
= bbar
.set_focus_index(2); // ignore errors
411 let mut inner
= LinearLayout
::vertical()
412 .child(PaddedView
::lrtb(
417 TextView
::new("END USER LICENSE AGREEMENT (EULA)").center(),
419 .child(Panel
::new(ScrollView
::new(
420 TextView
::new(get_eula()).center(),
422 .child(PaddedView
::lrtb(1, 1, 1, 0, bbar
));
424 let _
= inner
.set_focus_index(2); // ignore errors
426 InstallerView
::with_raw(state
, inner
)
429 fn bootdisk_dialog(siv
: &mut Cursive
) -> InstallerView
{
430 let state
= siv
.user_data
::<InstallerState
>().cloned().unwrap();
434 BootdiskOptionsView
::new(&state
.runtime_info
.disks
, &state
.options
.bootdisk
)
435 .with_name("bootdisk-options"),
437 let options
= siv
.call_on_name("bootdisk-options", BootdiskOptionsView
::get_values
);
440 Some(Ok(options
)) => {
441 siv
.with_user_data(|state
: &mut InstallerState
| {
442 state
.options
.bootdisk
= options
;
445 switch_to_next_screen(siv
, InstallerStep
::Timezone
, &timezone_dialog
);
448 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
449 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
456 fn timezone_dialog(siv
: &mut Cursive
) -> InstallerView
{
457 let state
= siv
.user_data
::<InstallerState
>().unwrap();
458 let options
= &state
.options
.timezone
;
462 TimezoneOptionsView
::new(&state
.locales
, options
).with_name("timezone-options"),
464 let options
= siv
.call_on_name("timezone-options", TimezoneOptionsView
::get_values
);
467 Some(Ok(options
)) => {
468 siv
.with_user_data(|state
: &mut InstallerState
| {
469 state
.options
.timezone
= options
;
472 switch_to_next_screen(siv
, InstallerStep
::Password
, &password_dialog
);
474 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
475 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
482 fn password_dialog(siv
: &mut Cursive
) -> InstallerView
{
483 let state
= siv
.user_data
::<InstallerState
>().unwrap();
484 let options
= &state
.options
.password
;
486 let inner
= FormView
::new()
487 .child("Root password", EditView
::new().secret())
488 .child("Confirm root password", EditView
::new().secret())
490 "Administator email",
491 EditView
::new().content(&options
.email
),
493 .with_name("password-options");
499 let options
= siv
.call_on_name("password-options", |view
: &mut FormView
| {
500 let root_password
= view
501 .get_value
::<EditView
, _
>(0)
502 .ok_or("failed to retrieve password")?
;
504 let confirm_password
= view
505 .get_value
::<EditView
, _
>(1)
506 .ok_or("failed to retrieve password confirmation")?
;
509 .get_value
::<EditView
, _
>(2)
510 .ok_or("failed to retrieve email")?
;
513 Regex
::new(r
"^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$")
516 if root_password
.len() < 5 {
517 Err("password too short")
518 } else if root_password
!= confirm_password
{
519 Err("passwords do not match")
520 } else if email
== "mail@example.invalid" {
521 Err("invalid email address")
522 } else if !email_regex
.is_match(&email
) {
523 Err("Email does not look like a valid address (user@domain.tld)")
533 Some(Ok(options
)) => {
534 siv
.with_user_data(|state
: &mut InstallerState
| {
535 state
.options
.password
= options
;
538 switch_to_next_screen(siv
, InstallerStep
::Network
, &network_dialog
);
540 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
541 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
548 fn network_dialog(siv
: &mut Cursive
) -> InstallerView
{
549 let state
= siv
.user_data
::<InstallerState
>().unwrap();
550 let options
= &state
.options
.network
;
552 let inner
= FormView
::new()
554 "Management interface",
557 .with_all_str(state
.runtime_info
.network
.interfaces
.keys()),
561 EditView
::new().content(options
.fqdn
.to_string()),
565 CidrAddressEditView
::new().content(options
.address
.clone()),
569 EditView
::new().content(options
.gateway
.to_string()),
572 "DNS server address",
573 EditView
::new().content(options
.dns_server
.to_string()),
575 .with_name("network-options");
581 let options
= siv
.call_on_name("network-options", |view
: &mut FormView
| {
583 .get_value
::<SelectView
, _
>(0)
584 .ok_or("failed to retrieve management interface name")?
;
587 .get_value
::<EditView
, _
>(1)
588 .ok_or("failed to retrieve host FQDN")?
590 .map_err(|_
| "failed to parse hostname".to_owned())?
;
593 .get_value
::<CidrAddressEditView
, _
>(2)
594 .ok_or("failed to retrieve host address")?
;
597 .get_value
::<EditView
, _
>(3)
598 .ok_or("failed to retrieve gateway address")?
600 .map_err(|err
| err
.to_string())?
;
602 let dns_server
= view
603 .get_value
::<EditView
, _
>(4)
604 .ok_or("failed to retrieve DNS server address")?
606 .map_err(|err
| err
.to_string())?
;
608 if address
.addr().is_ipv4() != gateway
.is_ipv4() {
609 Err("host and gateway IP address version must not differ".to_owned())
610 } else if address
.addr().is_ipv4() != dns_server
.is_ipv4() {
611 Err("host and DNS IP address version must not differ".to_owned())
612 } else if fqdn
.to_string().chars().all(|c
| c
.is_ascii_digit()) {
613 // Not supported/allowed on Debian
614 Err("hostname cannot be purely numeric".to_owned())
615 } else if fqdn
.to_string().ends_with(".invalid") {
616 Err("hostname does not look valid".to_owned())
629 Some(Ok(options
)) => {
630 siv
.with_user_data(|state
: &mut InstallerState
| {
631 state
.options
.network
= options
;
634 switch_to_next_screen(siv
, InstallerStep
::Summary
, &summary_dialog
);
636 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
637 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
644 pub struct SummaryOption
{
650 pub fn new
<S
: Into
<String
>>(name
: &'
static str, value
: S
) -> Self {
658 impl TableViewItem
for SummaryOption
{
659 fn get_column(&self, name
: &str) -> String
{
661 "name" => self.name
.to_owned(),
662 "value" => self.value
.clone(),
668 fn summary_dialog(siv
: &mut Cursive
) -> InstallerView
{
669 let state
= siv
.user_data
::<InstallerState
>().unwrap();
671 let mut bbar
= LinearLayout
::horizontal()
672 .child(abort_install_button())
673 .child(DummyView
.full_width())
674 .child(Button
::new("Previous", switch_to_prev_screen
))
676 .child(Button
::new("Install", |siv
| {
678 .find_name("reboot-after-install")
679 .map(|v
: ViewRef
<Checkbox
>| v
.is_checked())
680 .unwrap_or_default();
682 siv
.with_user_data(|state
: &mut InstallerState
| {
683 state
.options
.autoreboot
= autoreboot
;
686 switch_to_next_screen(siv
, InstallerStep
::Install
, &install_progress_dialog
);
689 let _
= bbar
.set_focus_index(2); // ignore errors
691 let mut inner
= LinearLayout
::vertical()
692 .child(PaddedView
::lrtb(
699 ("name".to_owned(), "Option".to_owned()),
700 ("value".to_owned(), "Selected value".to_owned()),
702 .items(state
.options
.to_summary(&state
.locales
)),
705 LinearLayout
::horizontal()
706 .child(DummyView
.full_width())
707 .child(Checkbox
::new().checked().with_name("reboot-after-install"))
709 TextView
::new(" Automatically reboot after successful installation").no_wrap(),
711 .child(DummyView
.full_width()),
713 .child(PaddedView
::lrtb(1, 1, 1, 0, bbar
));
715 let _
= inner
.set_focus_index(2); // ignore errors
717 InstallerView
::with_raw(state
, inner
)
720 fn install_progress_dialog(siv
: &mut Cursive
) -> InstallerView
{
721 // Ensure the screen is updated independently of keyboard events and such
722 siv
.set_autorefresh(true);
724 let cb_sink
= siv
.cb_sink().clone();
725 let state
= siv
.user_data
::<InstallerState
>().unwrap();
726 let progress_text
= TextContent
::new("starting the installation ..");
728 let progress_task
= {
729 let progress_text
= progress_text
.clone();
730 let options
= state
.options
.clone();
731 move |counter
: Counter
| {
733 use std
::process
::{Command, Stdio}
;
735 #[cfg(not(debug_assertions))]
736 let (path
, args
, envs
): (&str, [&str; 1], [(&str, &str); 0]) =
737 ("proxmox-low-level-installer", ["start-session"], []);
739 #[cfg(debug_assertions)]
740 let (path
, args
, envs
) = (
741 PathBuf
::from("./proxmox-low-level-installer"),
742 ["-t", "start-session-test"],
749 .stdin(Stdio
::piped())
750 .stdout(Stdio
::piped())
754 let mut child
= match child
{
757 let _
= cb_sink
.send(Box
::new(move |siv
| {
759 Dialog
::text(err
.to_string())
761 .button("Ok", Cursive
::quit
),
769 let reader
= child
.stdout
.take().map(BufReader
::new
)?
;
770 let mut writer
= child
.stdin
.take()?
;
772 serde_json
::to_writer(&mut writer
, &InstallConfig
::from(options
)).unwrap();
773 writeln
!(writer
).unwrap();
775 let writer
= Arc
::new(Mutex
::new(writer
));
777 for line
in reader
.lines() {
778 let line
= match line
{
783 let msg
= match line
.parse
::<UiMessage
>() {
786 eprintln
!("low-level installer: {stray}");
792 UiMessage
::Info(s
) => cb_sink
.send(Box
::new(|siv
| {
793 siv
.add_layer(Dialog
::info(s
).title("Information"));
795 UiMessage
::Error(s
) => cb_sink
.send(Box
::new(|siv
| {
796 siv
.add_layer(Dialog
::info(s
).title("Error"));
798 UiMessage
::Prompt(s
) => cb_sink
.send({
799 let writer
= writer
.clone();
800 Box
::new(move |siv
| {
806 let writer
= writer
.clone();
808 if let Ok(mut writer
) = writer
.lock() {
809 let _
= writeln
!(writer
, "ok");
814 if let Ok(mut writer
) = writer
.lock() {
815 let _
= writeln
!(writer
);
821 UiMessage
::Progress(ratio
, s
) => {
823 progress_text
.set_content(s
);
826 UiMessage
::Finished(success
, msg
) => {
828 progress_text
.set_content(msg
.to_owned());
829 cb_sink
.send(Box
::new(move |siv
| {
830 let title
= if success { "Success" }
else { "Failure" }
;
832 // For rebooting, we just need to quit the installer,
833 // our caller does the actual reboot.
837 .button("Reboot now", Cursive
::quit
),
841 .user_data
::<InstallerState
>()
842 .map(|state
| state
.options
.autoreboot
)
843 .unwrap_or_default();
845 if autoreboot
&& success
{
846 let cb_sink
= siv
.cb_sink();
848 let cb_sink
= cb_sink
.clone();
850 thread
::sleep(Duration
::from_secs(5));
851 let _
= cb_sink
.send(Box
::new(Cursive
::quit
));
864 if inner().is_none() {
866 .send(Box
::new(|siv
| {
868 Dialog
::text("low-level installer exited early")
870 .button("Exit", Cursive
::quit
),
878 let progress_bar
= ProgressBar
::new().with_task(progress_task
).full_width();
879 let inner
= PaddedView
::lrtb(
884 LinearLayout
::vertical()
885 .child(PaddedView
::lrtb(1, 1, 0, 0, progress_bar
))
887 .child(TextView
::new_with_content(progress_text
).center())
888 .child(PaddedView
::lrtb(
893 LinearLayout
::horizontal().child(abort_install_button()),
897 InstallerView
::with_raw(state
, inner
)
904 Finished(bool
, String
),
905 Progress(usize, String
),
908 impl FromStr
for UiMessage
{
911 fn from_str(s
: &str) -> Result
<Self, Self::Err
> {
912 let (ty
, rest
) = s
.split_once(": ").ok_or("invalid message: no type")?
;
915 "message" => Ok(UiMessage
::Info(rest
.to_owned())),
916 "error" => Ok(UiMessage
::Error(rest
.to_owned())),
917 "prompt" => Ok(UiMessage
::Prompt(rest
.to_owned())),
919 let (state
, rest
) = rest
.split_once(", ").ok_or("invalid message: no state")?
;
920 Ok(UiMessage
::Finished(state
== "ok", rest
.to_owned()))
923 let (percent
, rest
) = rest
.split_once(' '
).ok_or("invalid progress message")?
;
924 Ok(UiMessage
::Progress(
927 .map(|v
| (v
* 100.).floor() as usize)
928 .map_err(|err
| err
.to_string())?
,
932 unknown
=> Err(format
!("invalid message type {unknown}, rest: {rest}")),