1 #![forbid(unsafe_code)]
3 use std
::{collections::HashMap, env, net::IpAddr, path::PathBuf}
;
7 theme
::{ColorStyle, Effect, PaletteColor, Style}
,
8 view
::{Nameable, Offset, Resizable, ViewWrapper}
,
10 Button
, Checkbox
, Dialog
, DummyView
, EditView
, Layer
, LinearLayout
, PaddedView
, Panel
,
11 ProgressBar
, ResizedView
, ScrollView
, SelectView
, StackView
, TextContent
, TextView
,
14 Cursive
, CursiveRunnable
, ScreenId
, View
, XY
,
21 use setup
::{LocaleInfo, RuntimeInfo, SetupInfo}
;
30 BootdiskOptionsView
, CidrAddressEditView
, FormView
, TableView
, TableViewItem
,
34 // TextView::center() seems to garble the first two lines, so fix it manually here.
35 const PROXMOX_LOGO
: &str = r
#"
37 / __ \_________ _ ______ ___ ____ _ __
38 / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/
39 / ____/ / / /_/ /> </ / / / / / /_/ /> <
40 /_/ /_/ \____/_/|_/_/ /_/ /_/\____/_/|_| "#;
42 struct InstallerView
{
43 view
: ResizedView
<Dialog
>,
48 state
: &InstallerState
,
50 next_cb
: Box
<dyn Fn(&mut Cursive
)>,
52 let inner
= LinearLayout
::vertical()
53 .child(PaddedView
::lrtb(0, 0, 1, 1, view
))
54 .child(PaddedView
::lrtb(
59 LinearLayout
::horizontal()
60 .child(abort_install_button())
61 .child(DummyView
.full_width())
62 .child(Button
::new("Previous", switch_to_prev_screen
))
64 .child(Button
::new("Next", next_cb
)),
67 Self::with_raw(state
, inner
)
70 pub fn with_raw(state
: &InstallerState
, view
: impl View
) -> Self {
71 let setup
= &state
.setup_info
;
74 "{} ({}-{}) Installer",
75 setup
.config
.fullname
, setup
.iso_info
.release
, setup
.iso_info
.isorelease
78 let inner
= Dialog
::around(view
).title(title
);
81 // Limit the maximum to something reasonable, such that it won't get spread out much
82 // depending on the screen.
83 view
: ResizedView
::with_max_size((120, 40), inner
),
88 impl ViewWrapper
for InstallerView
{
89 cursive
::wrap_impl
!(self.view
: ResizedView
<Dialog
>);
92 struct InstallerBackgroundView
{
96 impl InstallerBackgroundView
{
97 pub fn new(state
: &InstallerState
) -> Self {
98 let product_name
= state
102 .trim_start_matches("Proxmox ");
104 let logo
= format
!("{PROXMOX_LOGO} {product_name}");
106 effects
: Effect
::Bold
.into(),
107 color
: ColorStyle
::back(PaletteColor
::View
),
110 let mut view
= StackView
::new();
111 view
.add_fullscreen_layer(Layer
::with_color(
114 .fixed_height(logo
.lines().count() + 1),
115 ColorStyle
::back(PaletteColor
::View
),
117 view
.add_transparent_layer_at(
120 y
: Offset
::Absolute(0),
122 TextView
::new(logo
).style(style
),
129 impl ViewWrapper
for InstallerBackgroundView
{
130 cursive
::wrap_impl
!(self.view
: StackView
);
133 #[derive(Clone, Eq, Hash, PartialEq)]
145 struct InstallerState
{
146 options
: InstallerOptions
,
147 available_disks
: Vec
<Disk
>,
148 setup_info
: SetupInfo
,
149 runtime_info
: RuntimeInfo
,
151 steps
: HashMap
<InstallerStep
, ScreenId
>,
156 let mut siv
= cursive
::termion();
158 let in_test_mode
= match env
::args().nth(1).as_deref() {
161 // Always force the test directory in debug builds
162 #[cfg(debug_assertions)]
165 #[cfg(not(debug_assertions))]
169 let (setup_info
, locales
, runtime_info
) = match installer_setup(in_test_mode
) {
170 Ok(result
) => result
,
171 Err(err
) => initial_setup_error(&mut siv
, &err
),
174 siv
.clear_global_callbacks(Event
::CtrlChar('c'
));
175 siv
.set_on_pre_event(Event
::CtrlChar('c'
), trigger_abort_install_dialog
);
177 let available_disks
: Vec
<Disk
> = runtime_info
180 .map(|(name
, info
)| Disk
{
181 path
: format
!("/dev/{name}"),
186 siv
.set_user_data(InstallerState
{
187 options
: InstallerOptions
{
188 bootdisk
: BootdiskOptions
::defaults_from(&available_disks
[0]),
189 timezone
: TimezoneOptions
::default(),
190 password
: PasswordOptions
::default(),
191 network
: NetworkOptions
::from(&runtime_info
.network
),
198 steps
: HashMap
::new(),
202 switch_to_next_screen(&mut siv
, InstallerStep
::Licence
, &license_dialog
);
206 fn installer_setup(in_test_mode
: bool
) -> Result
<(SetupInfo
, LocaleInfo
, RuntimeInfo
), String
> {
207 system
::has_min_requirements()?
;
209 let base_path
= if in_test_mode { "./testdir" }
else { "/" }
;
210 let mut path
= PathBuf
::from(base_path
);
213 path
.push("proxmox-installer");
215 let installer_info
= {
216 let mut path
= path
.clone();
217 path
.push("iso-info.json");
219 setup
::read_json(&path
).map_err(|err
| format
!("Failed to retrieve setup info: {err}"))?
223 let mut path
= path
.clone();
224 path
.push("locales.json");
226 setup
::read_json(&path
).map_err(|err
| format
!("Failed to retrieve locale info: {err}"))?
229 let runtime_info
: RuntimeInfo
= {
230 let mut path
= path
.clone();
231 path
.push("run-env-info.json");
233 setup
::read_json(&path
)
234 .map_err(|err
| format
!("Failed to retrieve runtime environment info: {err}"))?
237 if runtime_info
.disks
.is_empty() {
238 Err("The installer could not find any supported hard disks.".to_owned())
240 Ok((installer_info
, locale_info
, runtime_info
))
244 fn initial_setup_error(siv
: &mut CursiveRunnable
, message
: &str) -> ! {
246 Dialog
::around(TextView
::new(message
))
247 .title("Installer setup error")
248 .button("Ok", Cursive
::quit
),
252 std
::process
::exit(1);
255 fn switch_to_next_screen(
258 constructor
: &dyn Fn(&mut Cursive
) -> InstallerView
,
260 let state
= siv
.user_data
::<InstallerState
>().cloned().unwrap();
262 // Check if the screen already exists; if yes, then simply switch to it.
263 if let Some(screen_id
) = state
.steps
.get(&step
) {
264 siv
.set_screen(*screen_id
);
268 let v
= constructor(siv
);
269 let screen
= siv
.add_active_screen();
270 siv
.with_user_data(|state
: &mut InstallerState
| state
.steps
.insert(step
, screen
));
272 if let Some(state
) = siv
.user_data
::<InstallerState
>().cloned() {
273 siv
.screen_mut().add_transparent_layer_at(
275 x
: Offset
::Parent(0),
276 y
: Offset
::Parent(0),
278 InstallerBackgroundView
::new(&state
),
282 siv
.screen_mut().add_layer(v
);
285 fn switch_to_prev_screen(siv
: &mut Cursive
) {
286 let id
= siv
.active_screen().saturating_sub(1);
290 #[cfg(not(debug_assertions))]
295 callback
: &'
static dyn Fn(&mut Cursive
),
298 Dialog
::around(TextView
::new(text
))
300 .dismiss_button("No")
301 .button("Yes", callback
),
305 fn trigger_abort_install_dialog(siv
: &mut Cursive
) {
306 #[cfg(debug_assertions)]
309 #[cfg(not(debug_assertions))]
312 "Abort installation?",
313 "Are you sure you want to abort the installation?",
318 fn abort_install_button() -> Button
{
319 Button
::new("Abort", trigger_abort_install_dialog
)
322 fn get_eula() -> String
{
323 // TODO: properly using info from Proxmox::Install::Env::setup()
324 std
::fs
::read_to_string("/cdrom/EULA")
325 .unwrap_or_else(|_
| "< Debug build - ignoring non-existing EULA >".to_owned())
328 fn license_dialog(siv
: &mut Cursive
) -> InstallerView
{
329 let state
= siv
.user_data
::<InstallerState
>().unwrap();
331 let inner
= LinearLayout
::vertical()
332 .child(PaddedView
::lrtb(
337 TextView
::new("END USER LICENSE AGREEMENT (EULA)").center(),
339 .child(Panel
::new(ScrollView
::new(
340 TextView
::new(get_eula()).center(),
342 .child(PaddedView
::lrtb(
347 LinearLayout
::horizontal()
348 .child(abort_install_button())
349 .child(DummyView
.full_width())
350 .child(Button
::new("I agree", |siv
| {
351 switch_to_next_screen(siv
, InstallerStep
::Bootdisk
, &bootdisk_dialog
)
355 InstallerView
::with_raw(state
, inner
)
358 fn bootdisk_dialog(siv
: &mut Cursive
) -> InstallerView
{
359 let state
= siv
.user_data
::<InstallerState
>().cloned().unwrap();
363 BootdiskOptionsView
::new(&state
.available_disks
, &state
.options
.bootdisk
)
364 .with_name("bootdisk-options"),
367 .call_on_name("bootdisk-options", BootdiskOptionsView
::get_values
)
370 if let Some(options
) = options
{
371 siv
.with_user_data(|state
: &mut InstallerState
| {
372 state
.options
.bootdisk
= options
;
375 switch_to_next_screen(siv
, InstallerStep
::Timezone
, &timezone_dialog
);
377 siv
.add_layer(Dialog
::info("Invalid values"));
383 fn timezone_dialog(siv
: &mut Cursive
) -> InstallerView
{
384 let state
= siv
.user_data
::<InstallerState
>().unwrap();
385 let options
= &state
.options
.timezone
;
389 TimezoneOptionsView
::new(&state
.locales
, options
).with_name("timezone-options"),
391 let options
= siv
.call_on_name("timezone-options", TimezoneOptionsView
::get_values
);
394 Some(Ok(options
)) => {
395 siv
.with_user_data(|state
: &mut InstallerState
| {
396 state
.options
.timezone
= options
;
399 switch_to_next_screen(siv
, InstallerStep
::Password
, &password_dialog
);
401 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
402 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
408 fn password_dialog(siv
: &mut Cursive
) -> InstallerView
{
409 let state
= siv
.user_data
::<InstallerState
>().unwrap();
410 let options
= &state
.options
.password
;
412 let inner
= FormView
::new()
413 .child("Root password", EditView
::new().secret())
414 .child("Confirm root password", EditView
::new().secret())
416 "Administator email",
417 EditView
::new().content(&options
.email
),
419 .with_name("password-options");
425 let options
= siv
.call_on_name("password-options", |view
: &mut FormView
| {
426 let root_password
= view
427 .get_value
::<EditView
, _
>(0)
428 .ok_or("failed to retrieve password")?
;
430 let confirm_password
= view
431 .get_value
::<EditView
, _
>(1)
432 .ok_or("failed to retrieve password confirmation")?
;
435 .get_value
::<EditView
, _
>(2)
436 .ok_or("failed to retrieve email")?
;
438 if root_password
.len() < 5 {
439 Err("password too short")
440 } else if root_password
!= confirm_password
{
441 Err("passwords do not match")
442 } else if email
== "mail@example.invalid" {
443 Err("invalid email address")
453 Some(Ok(options
)) => {
454 siv
.with_user_data(|state
: &mut InstallerState
| {
455 state
.options
.password
= options
;
458 switch_to_next_screen(siv
, InstallerStep
::Network
, &network_dialog
);
460 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
461 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
467 fn network_dialog(siv
: &mut Cursive
) -> InstallerView
{
468 let state
= siv
.user_data
::<InstallerState
>().unwrap();
469 let options
= &state
.options
.network
;
471 let inner
= FormView
::new()
473 "Management interface",
474 SelectView
::new().popup().with_all_str(vec
!["eth0"]),
478 EditView
::new().content(options
.fqdn
.to_string()),
482 CidrAddressEditView
::new().content(options
.address
.clone()),
486 EditView
::new().content(options
.gateway
.to_string()),
489 "DNS server address",
490 EditView
::new().content(options
.dns_server
.to_string()),
492 .with_name("network-options");
498 let options
= siv
.call_on_name("network-options", |view
: &mut FormView
| {
500 .get_value
::<SelectView
, _
>(0)
501 .ok_or("failed to retrieve management interface name")?
;
504 .get_value
::<EditView
, _
>(1)
505 .ok_or("failed to retrieve host FQDN")?
507 .map_err(|_
| "failed to parse hostname".to_owned())?
;
510 .get_value
::<CidrAddressEditView
, _
>(2)
511 .ok_or("failed to retrieve host address")?
;
514 .get_value
::<EditView
, _
>(3)
515 .ok_or("failed to retrieve gateway address")?
517 .map_err(|err
| err
.to_string())?
;
519 let dns_server
= view
520 .get_value
::<EditView
, _
>(3)
521 .ok_or("failed to retrieve DNS server address")?
523 .map_err(|err
| err
.to_string())?
;
525 if address
.addr().is_ipv4() != gateway
.is_ipv4() {
526 Err("host and gateway IP address version must not differ".to_owned())
527 } else if address
.addr().is_ipv4() != dns_server
.is_ipv4() {
528 Err("host and DNS IP address version must not differ".to_owned())
529 } else if fqdn
.to_string().chars().all(|c
| c
.is_ascii_digit()) {
530 // Not supported/allowed on Debian
531 Err("hostname cannot be purely numeric".to_owned())
532 } else if fqdn
.to_string().ends_with(".invalid") {
533 Err("hostname does not look valid".to_owned())
546 Some(Ok(options
)) => {
547 siv
.with_user_data(|state
: &mut InstallerState
| {
548 state
.options
.network
= options
;
551 switch_to_next_screen(siv
, InstallerStep
::Summary
, &summary_dialog
);
553 Some(Err(err
)) => siv
.add_layer(Dialog
::info(format
!("Invalid values: {err}"))),
554 _
=> siv
.add_layer(Dialog
::info("Invalid values")),
560 pub struct SummaryOption
{
566 pub fn new
<S
: Into
<String
>>(name
: &'
static str, value
: S
) -> Self {
574 impl TableViewItem
for SummaryOption
{
575 fn get_column(&self, name
: &str) -> String
{
577 "name" => self.name
.to_owned(),
578 "value" => self.value
.clone(),
584 fn summary_dialog(siv
: &mut Cursive
) -> InstallerView
{
585 let state
= siv
.user_data
::<InstallerState
>().unwrap();
587 let inner
= LinearLayout
::vertical()
588 .child(PaddedView
::lrtb(
595 ("name".to_owned(), "Option".to_owned()),
596 ("value".to_owned(), "Selected value".to_owned()),
598 .items(state
.options
.to_summary(&state
.locales
)),
601 LinearLayout
::horizontal()
602 .child(DummyView
.full_width())
603 .child(Checkbox
::new().with_name("reboot-after-install"))
605 TextView
::new(" Automatically reboot after successful installation").no_wrap(),
607 .child(DummyView
.full_width()),
609 .child(PaddedView
::lrtb(
614 LinearLayout
::horizontal()
615 .child(abort_install_button())
616 .child(DummyView
.full_width())
617 .child(Button
::new("Previous", switch_to_prev_screen
))
619 .child(Button
::new("Install", |siv
| {
621 .find_name("reboot-after-install")
622 .map(|v
: ViewRef
<Checkbox
>| v
.is_checked())
623 .unwrap_or_default();
625 siv
.with_user_data(|state
: &mut InstallerState
| {
626 state
.options
.reboot
= reboot
;
629 switch_to_next_screen(siv
, InstallerStep
::Install
, &install_progress_dialog
);
633 InstallerView
::with_raw(state
, inner
)
636 fn install_progress_dialog(siv
: &mut Cursive
) -> InstallerView
{
637 // Ensure the screen is updated independently of keyboard events and such
638 siv
.set_autorefresh(true);
640 let state
= siv
.user_data
::<InstallerState
>().unwrap();
641 let progress_text
= TextContent
::new("extracting ..");
642 let progress_bar
= ProgressBar
::new()
646 std
::thread
::sleep(std
::time
::Duration
::from_millis(50));
653 let inner
= PaddedView
::lrtb(
658 LinearLayout
::vertical()
659 .child(PaddedView
::lrtb(1, 1, 0, 0, progress_bar
))
661 .child(TextView
::new_with_content(progress_text
).center()),
664 InstallerView
::with_raw(state
, inner
)