1 #![forbid(unsafe_code)]
3 use std
::{collections::HashMap, env, net::IpAddr, path::PathBuf}
;
7 view
::{Nameable, Resizable, ViewWrapper}
,
9 Button
, Checkbox
, Dialog
, DummyView
, EditView
, LinearLayout
, PaddedView
, Panel
,
10 ProgressBar
, ResizedView
, ScrollView
, SelectView
, TextContent
, TextView
, ViewRef
,
12 Cursive
, CursiveRunnable
, ScreenId
, View
,
19 use setup
::{LocaleInfo, ProxmoxProduct, RuntimeInfo, SetupInfo}
;
28 BootdiskOptionsView
, CidrAddressEditView
, FormView
, TableView
, TableViewItem
,
32 // TextView::center() seems to garble the first two lines, so fix it manually here.
33 const LOGO_PVE
: &str = r
#"
35 / __ \_________ _ ______ ___ ____ _ __ | | / / ____/
36 / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/ | | / / __/
37 / ____/ / / /_/ /> </ / / / / / /_/ /> < | |/ / /___
38 /_/ /_/ \____/_/|_/_/ /_/ /_/\____/_/|_| |___/_____/
41 const LOGO_PBS
: &str = r
#"
43 / __ \_________ _ ______ ___ ____ _ __ / __ ) ___/
44 / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/ / __ \__ \
45 / ____/ / / /_/ /> </ / / / / / /_/ /> < / /_/ /__/ /
46 /_/ /_/ \____/_/|_/_/ /_/ /_/\____/_/|_| /_____/____/
49 const LOGO_PMG
: &str = r
#"
51 / __ \_________ _ ______ ___ ____ _ __ / |/ / ____/
52 / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/ / /|_/ / / __
53 / ____/ / / /_/ /> </ / / / / / /_/ /> < / / / / /_/ /
54 /_/ /_/ \____/_/|_/_/ /_/ /_/\____/_/|_| /_/ /_/\____/
57 struct InstallerView
{
58 view
: ResizedView
<LinearLayout
>,
63 state
: &InstallerState
,
65 next_cb
: Box
<dyn Fn(&mut Cursive
)>,
67 let inner
= LinearLayout
::vertical()
68 .child(PaddedView
::lrtb(0, 0, 1, 1, view
))
69 .child(PaddedView
::lrtb(
74 LinearLayout
::horizontal()
75 .child(abort_install_button())
76 .child(DummyView
.full_width())
77 .child(Button
::new("Previous", switch_to_prev_screen
))
79 .child(Button
::new("Next", next_cb
)),
82 Self::with_raw(state
, inner
)
85 pub fn with_raw(state
: &InstallerState
, view
: impl View
) -> Self {
86 let setup
= &state
.setup_info
;
88 let logo
= match setup
.config
.product
{
89 ProxmoxProduct
::PVE
=> LOGO_PVE
,
90 ProxmoxProduct
::PBS
=> LOGO_PBS
,
91 ProxmoxProduct
::PMG
=> LOGO_PMG
,
95 "{} ({}-{}) Installer",
96 setup
.config
.fullname
, setup
.iso_info
.release
, setup
.iso_info
.isorelease
99 let inner
= LinearLayout
::vertical()
100 .child(PaddedView
::lrtb(1, 1, 0, 1, TextView
::new(logo
).center()))
101 .child(Dialog
::around(view
).title(title
));
104 // Limit the maximum to something reasonable, such that it won't get spread out much
105 // depending on the screen.
106 view
: ResizedView
::with_max_size((120, 40), inner
),
111 impl ViewWrapper
for InstallerView
{
112 cursive
::wrap_impl
!(self.view
: ResizedView
<LinearLayout
>);
115 #[derive(Clone, Eq, Hash, PartialEq)]
127 struct InstallerState
{
128 options
: InstallerOptions
,
129 available_disks
: Vec
<Disk
>,
130 setup_info
: SetupInfo
,
131 runtime_info
: RuntimeInfo
,
133 steps
: HashMap
<InstallerStep
, ScreenId
>,
138 let mut siv
= cursive
::termion();
140 let in_test_mode
= match env
::args().nth(1).as_deref() {
143 // Always force the test directory in debug builds
144 #[cfg(debug_assertions)]
147 #[cfg(not(debug_assertions))]
151 let (setup_info
, locales
, runtime_info
) = match installer_setup(in_test_mode
) {
152 Ok(result
) => result
,
153 Err(err
) => initial_setup_error(&mut siv
, &err
),
156 siv
.clear_global_callbacks(Event
::CtrlChar('c'
));
157 siv
.set_on_pre_event(Event
::CtrlChar('c'
), trigger_abort_install_dialog
);
159 let available_disks
: Vec
<Disk
> = runtime_info
162 .map(|(name
, info
)| Disk
{
163 path
: format
!("/dev/{name}"),
168 siv
.set_user_data(InstallerState
{
169 options
: InstallerOptions
{
170 bootdisk
: BootdiskOptions
::defaults_from(&available_disks
[0]),
171 timezone
: TimezoneOptions
::default(),
172 password
: PasswordOptions
::default(),
173 network
: NetworkOptions
::default(),
180 steps
: HashMap
::new(),
184 switch_to_next_screen(&mut siv
, InstallerStep
::Licence
, &license_dialog
);
188 fn installer_setup(in_test_mode
: bool
) -> Result
<(SetupInfo
, LocaleInfo
, RuntimeInfo
), String
> {
189 system
::has_min_requirements()?
;
191 let base_path
= if in_test_mode { "./testdir" }
else { "/" }
;
192 let mut path
= PathBuf
::from(base_path
);
195 path
.push("proxmox-installer");
197 let installer_info
= {
198 let mut path
= path
.clone();
199 path
.push("iso-info.json");
201 setup
::read_json(&path
).map_err(|err
| format
!("Failed to retrieve setup info: {err}"))?
205 let mut path
= path
.clone();
206 path
.push("locales.json");
208 setup
::read_json(&path
).map_err(|err
| format
!("Failed to retrieve locale info: {err}"))?
212 let mut path
= path
.clone();
213 path
.push("run-env-info.json");
215 setup
::read_json(&path
)
216 .map_err(|err
| format
!("Failed to retrieve runtime environment info: {err}"))?
219 Ok((installer_info
, locale_info
, runtime_info
))
222 fn initial_setup_error(siv
: &mut CursiveRunnable
, message
: &str) -> ! {
224 Dialog
::around(TextView
::new(message
))
225 .title("Installer setup error")
226 .button("Ok", Cursive
::quit
),
230 std
::process
::exit(1);
233 fn switch_to_next_screen(
236 constructor
: &dyn Fn(&mut Cursive
) -> InstallerView
,
238 // Check if the screen already exists; if yes, then simply switch to it.
239 if let Some(state
) = siv
.user_data
::<InstallerState
>().cloned() {
240 if let Some(screen_id
) = state
.steps
.get(&step
) {
241 siv
.set_screen(*screen_id
);
246 let v
= constructor(siv
);
247 let screen
= siv
.add_active_screen();
248 siv
.with_user_data(|state
: &mut InstallerState
| state
.steps
.insert(step
, screen
));
249 siv
.screen_mut().add_layer(v
);
252 fn switch_to_prev_screen(siv
: &mut Cursive
) {
253 let id
= siv
.active_screen().saturating_sub(1);
257 #[cfg(not(debug_assertions))]
262 callback
: &'
static dyn Fn(&mut Cursive
),
265 Dialog
::around(TextView
::new(text
))
267 .dismiss_button("No")
268 .button("Yes", callback
),
272 fn trigger_abort_install_dialog(siv
: &mut Cursive
) {
273 #[cfg(debug_assertions)]
276 #[cfg(not(debug_assertions))]
279 "Abort installation?",
280 "Are you sure you want to abort the installation?",
285 fn abort_install_button() -> Button
{
286 Button
::new("Abort", trigger_abort_install_dialog
)
289 fn get_eula() -> String
{
290 // TODO: properly using info from Proxmox::Install::Env::setup()
291 std
::fs
::read_to_string("/cdrom/EULA")
292 .unwrap_or_else(|_
| "< Debug build - ignoring non-existing EULA >".to_owned())
295 fn license_dialog(siv
: &mut Cursive
) -> InstallerView
{
296 let state
= siv
.user_data
::<InstallerState
>().unwrap();
298 let inner
= LinearLayout
::vertical()
299 .child(PaddedView
::lrtb(
304 TextView
::new("END USER LICENSE AGREEMENT (EULA)").center(),
306 .child(Panel
::new(ScrollView
::new(
307 TextView
::new(get_eula()).center(),
309 .child(PaddedView
::lrtb(
314 LinearLayout
::horizontal()
315 .child(abort_install_button())
316 .child(DummyView
.full_width())
317 .child(Button
::new("I agree", |siv
| {
318 switch_to_next_screen(siv
, InstallerStep
::Bootdisk
, &bootdisk_dialog
)
322 InstallerView
::with_raw(state
, inner
)
325 fn bootdisk_dialog(siv
: &mut Cursive
) -> InstallerView
{
326 let state
= siv
.user_data
::<InstallerState
>().cloned().unwrap();
330 BootdiskOptionsView
::new(&state
.available_disks
, &state
.options
.bootdisk
)
331 .with_name("bootdisk-options"),
334 .call_on_name("bootdisk-options", BootdiskOptionsView
::get_values
)
337 if let Some(options
) = options
{
338 siv
.with_user_data(|state
: &mut InstallerState
| {
339 state
.options
.bootdisk
= options
;
342 switch_to_next_screen(siv
, InstallerStep
::Timezone
, &timezone_dialog
);
344 siv
.add_layer(Dialog
::info("Invalid values"));
350 fn timezone_dialog(siv
: &mut Cursive
) -> InstallerView
{
351 let state
= siv
.user_data
::<InstallerState
>().unwrap();
352 let options
= &state
.options
.timezone
;
356 TimezoneOptionsView
::new(&state
.locales
, options
).with_name("timezone-options"),
358 let options
= siv
.call_on_name("timezone-options", TimezoneOptionsView
::get_values
);
361 Some(Ok(options
)) => {
362 siv
.with_user_data(|state
: &mut InstallerState
| {
363 state
.options
.timezone
= options
;
366 switch_to_next_screen(siv
, InstallerStep
::Password
, &password_dialog
);
368 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
369 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
375 fn password_dialog(siv
: &mut Cursive
) -> InstallerView
{
376 let state
= siv
.user_data
::<InstallerState
>().unwrap();
377 let options
= &state
.options
.password
;
379 let inner
= FormView
::new()
380 .child("Root password", EditView
::new().secret())
381 .child("Confirm root password", EditView
::new().secret())
383 "Administator email",
384 EditView
::new().content(&options
.email
),
386 .with_name("password-options");
392 let options
= siv
.call_on_name("password-options", |view
: &mut FormView
| {
393 let root_password
= view
394 .get_value
::<EditView
, _
>(0)
395 .ok_or("failed to retrieve password")?
;
397 let confirm_password
= view
398 .get_value
::<EditView
, _
>(1)
399 .ok_or("failed to retrieve password confirmation")?
;
402 .get_value
::<EditView
, _
>(2)
403 .ok_or("failed to retrieve email")?
;
405 if root_password
.len() < 5 {
406 Err("password too short")
407 } else if root_password
!= confirm_password
{
408 Err("passwords do not match")
409 } else if email
== "mail@example.invalid" {
410 Err("invalid email address")
420 Some(Ok(options
)) => {
421 siv
.with_user_data(|state
: &mut InstallerState
| {
422 state
.options
.password
= options
;
425 switch_to_next_screen(siv
, InstallerStep
::Network
, &network_dialog
);
427 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
428 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
434 fn network_dialog(siv
: &mut Cursive
) -> InstallerView
{
435 let state
= siv
.user_data
::<InstallerState
>().unwrap();
436 let options
= &state
.options
.network
;
438 let inner
= FormView
::new()
440 "Management interface",
441 SelectView
::new().popup().with_all_str(vec
!["eth0"]),
445 EditView
::new().content(options
.fqdn
.to_string()),
449 CidrAddressEditView
::new().content(options
.address
.clone()),
453 EditView
::new().content(options
.gateway
.to_string()),
456 "DNS server address",
457 EditView
::new().content(options
.dns_server
.to_string()),
459 .with_name("network-options");
465 let options
= siv
.call_on_name("network-options", |view
: &mut FormView
| {
467 .get_value
::<SelectView
, _
>(0)
468 .ok_or("failed to retrieve management interface name")?
;
471 .get_value
::<EditView
, _
>(1)
472 .ok_or("failed to retrieve host FQDN")?
474 .map_err(|_
| "failed to parse hostname".to_owned())?
;
477 .get_value
::<CidrAddressEditView
, _
>(2)
478 .ok_or("failed to retrieve host address")?
;
481 .get_value
::<EditView
, _
>(3)
482 .ok_or("failed to retrieve gateway address")?
484 .map_err(|err
| err
.to_string())?
;
486 let dns_server
= view
487 .get_value
::<EditView
, _
>(3)
488 .ok_or("failed to retrieve DNS server address")?
490 .map_err(|err
| err
.to_string())?
;
492 if address
.addr().is_ipv4() != gateway
.is_ipv4() {
493 Err("host and gateway IP address version must not differ".to_owned())
494 } else if address
.addr().is_ipv4() != dns_server
.is_ipv4() {
495 Err("host and DNS IP address version must not differ".to_owned())
496 } else if fqdn
.to_string().chars().all(|c
| c
.is_ascii_digit()) {
497 // Not supported/allowed on Debian
498 Err("hostname cannot be purely numeric".to_owned())
499 } else if fqdn
.to_string().ends_with(".invalid") {
500 Err("hostname does not look valid".to_owned())
513 Some(Ok(options
)) => {
514 siv
.with_user_data(|state
: &mut InstallerState
| {
515 state
.options
.network
= options
;
518 switch_to_next_screen(siv
, InstallerStep
::Summary
, &summary_dialog
);
520 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
521 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
527 pub struct SummaryOption
{
533 pub fn new
<S
: Into
<String
>>(name
: &'
static str, value
: S
) -> Self {
541 impl TableViewItem
for SummaryOption
{
542 fn get_column(&self, name
: &str) -> String
{
544 "name" => self.name
.to_owned(),
545 "value" => self.value
.clone(),
551 fn summary_dialog(siv
: &mut Cursive
) -> InstallerView
{
552 let state
= siv
.user_data
::<InstallerState
>().unwrap();
554 let inner
= LinearLayout
::vertical()
555 .child(PaddedView
::lrtb(
562 ("name".to_owned(), "Option".to_owned()),
563 ("value".to_owned(), "Selected value".to_owned()),
565 .items(state
.options
.to_summary(&state
.locales
)),
568 LinearLayout
::horizontal()
569 .child(DummyView
.full_width())
570 .child(Checkbox
::new().with_name("reboot-after-install"))
572 TextView
::new(" Automatically reboot after successful installation").no_wrap(),
574 .child(DummyView
.full_width()),
576 .child(PaddedView
::lrtb(
581 LinearLayout
::horizontal()
582 .child(abort_install_button())
583 .child(DummyView
.full_width())
584 .child(Button
::new("Previous", switch_to_prev_screen
))
586 .child(Button
::new("Install", |siv
| {
588 .find_name("reboot-after-install")
589 .map(|v
: ViewRef
<Checkbox
>| v
.is_checked())
590 .unwrap_or_default();
592 siv
.with_user_data(|state
: &mut InstallerState
| {
593 state
.options
.reboot
= reboot
;
596 switch_to_next_screen(siv
, InstallerStep
::Install
, &install_progress_dialog
);
600 InstallerView
::with_raw(state
, inner
)
603 fn install_progress_dialog(siv
: &mut Cursive
) -> InstallerView
{
604 // Ensure the screen is updated independently of keyboard events and such
605 siv
.set_autorefresh(true);
607 let state
= siv
.user_data
::<InstallerState
>().unwrap();
608 let progress_text
= TextContent
::new("extracting ..");
609 let progress_bar
= ProgressBar
::new()
613 std
::thread
::sleep(std
::time
::Duration
::from_millis(50));
620 let inner
= PaddedView
::lrtb(
625 LinearLayout
::vertical()
626 .child(PaddedView
::lrtb(1, 1, 0, 0, progress_bar
))
628 .child(TextView
::new_with_content(progress_text
).center()),
631 InstallerView
::with_raw(state
, inner
)