]> git.proxmox.com Git - pve-installer.git/blame - proxmox-tui-installer/src/main.rs
tui: bootdisk: expose `arc_max` ZFS option for PVE installations
[pve-installer.git] / proxmox-tui-installer / src / main.rs
CommitLineData
2789d95b
CH
1#![forbid(unsafe_code)]
2
3285f0ad
CH
3use std::{
4 collections::HashMap,
5 env,
6 io::{BufRead, BufReader, Write},
7 net::IpAddr,
3285f0ad
CH
8 str::FromStr,
9 sync::{Arc, Mutex},
7111932d
CH
10 thread,
11 time::Duration,
3285f0ad 12};
83071f35 13
183e2a76 14use cursive::{
ccf3b075 15 event::Event,
08ad8ed6 16 theme::{ColorStyle, Effect, PaletteColor, Style},
3285f0ad 17 utils::Counter,
08ad8ed6 18 view::{Nameable, Offset, Resizable, ViewWrapper},
183e2a76 19 views::{
08ad8ed6
CH
20 Button, Checkbox, Dialog, DummyView, EditView, Layer, LinearLayout, PaddedView, Panel,
21 ProgressBar, ResizedView, ScrollView, SelectView, StackView, TextContent, TextView,
22 ViewRef,
183e2a76 23 },
08ad8ed6 24 Cursive, CursiveRunnable, ScreenId, View, XY,
183e2a76 25};
32368ac3 26
298a4de0
DC
27use regex::Regex;
28
32368ac3 29mod options;
86c48f76
AL
30use options::InstallerOptions;
31
32use proxmox_installer_common::{
33 options::{BootdiskOptions, NetworkOptions, PasswordOptions, TimezoneOptions},
9a848580 34 setup::{installer_setup, LocaleInfo, ProxmoxProduct, RuntimeInfo, SetupInfo},
86c48f76
AL
35 utils::Fqdn,
36};
32368ac3
WB
37
38mod setup;
86c48f76 39use setup::InstallConfig;
32368ac3
WB
40
41mod system;
42
32368ac3 43mod views;
e1a11f79
CH
44use views::{
45 BootdiskOptionsView, CidrAddressEditView, FormView, TableView, TableViewItem,
46 TimezoneOptionsView,
47};
183e2a76
CH
48
49// TextView::center() seems to garble the first two lines, so fix it manually here.
08ad8ed6 50const PROXMOX_LOGO: &str = r#"
e730b3fe
CH
51 ____
52| _ \ _ __ _____ ___ __ ___ _____ __
53| |_) | '__/ _ \ \/ / '_ ` _ \ / _ \ \/ /
54| __/| | | (_) > <| | | | | | (_) > <
55|_| |_| \___/_/\_\_| |_| |_|\___/_/\_\ "#;
183e2a76
CH
56
57struct InstallerView {
08ad8ed6 58 view: ResizedView<Dialog>,
183e2a76
CH
59}
60
61impl InstallerView {
90c0ea37
CH
62 pub fn new<T: View>(
63 state: &InstallerState,
64 view: T,
65 next_cb: Box<dyn Fn(&mut Cursive)>,
1d8fcd72 66 focus_next: bool,
90c0ea37 67 ) -> Self {
1d8fcd72
DC
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))
72 .child(DummyView)
73 .child(Button::new("Next", next_cb));
74 let _ = bbar.set_focus_index(4); // ignore errors
75 let mut inner = LinearLayout::vertical()
8807d547 76 .child(PaddedView::lrtb(0, 0, 1, 1, view))
1d8fcd72
DC
77 .child(PaddedView::lrtb(1, 1, 0, 0, bbar));
78 if focus_next {
79 let _ = inner.set_focus_index(1); // ignore errors
80 }
f522afb1 81
90c0ea37 82 Self::with_raw(state, inner)
f522afb1
CH
83 }
84
90c0ea37
CH
85 pub fn with_raw(state: &InstallerState, view: impl View) -> Self {
86 let setup = &state.setup_info;
87
90c0ea37
CH
88 let title = format!(
89 "{} ({}-{}) Installer",
fbfd1838 90 setup.config.fullname, setup.iso_info.release, setup.iso_info.isorelease
90c0ea37
CH
91 );
92
08ad8ed6 93 let inner = Dialog::around(view).title(title);
8b43c2d3 94
183e2a76 95 Self {
8b43c2d3
CH
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),
183e2a76
CH
99 }
100 }
101}
102
103impl ViewWrapper for InstallerView {
08ad8ed6
CH
104 cursive::wrap_impl!(self.view: ResizedView<Dialog>);
105}
106
107struct InstallerBackgroundView {
108 view: StackView,
109}
110
111impl InstallerBackgroundView {
1936156e 112 pub fn new() -> Self {
08ad8ed6
CH
113 let style = Style {
114 effects: Effect::Bold.into(),
115 color: ColorStyle::back(PaletteColor::View),
116 };
117
118 let mut view = StackView::new();
119 view.add_fullscreen_layer(Layer::with_color(
120 DummyView
121 .full_width()
1936156e 122 .fixed_height(PROXMOX_LOGO.lines().count() + 1),
08ad8ed6
CH
123 ColorStyle::back(PaletteColor::View),
124 ));
125 view.add_transparent_layer_at(
126 XY {
127 x: Offset::Center,
128 y: Offset::Absolute(0),
129 },
1936156e 130 TextView::new(PROXMOX_LOGO).style(style),
08ad8ed6
CH
131 );
132
133 Self { view }
134 }
135}
136
137impl ViewWrapper for InstallerBackgroundView {
138 cursive::wrap_impl!(self.view: StackView);
183e2a76
CH
139}
140
6ab6af4c
CH
141#[derive(Clone, Eq, Hash, PartialEq)]
142enum InstallerStep {
143 Licence,
144 Bootdisk,
145 Timezone,
146 Password,
147 Network,
148 Summary,
81f9348c 149 Install,
6ab6af4c
CH
150}
151
ed191e60 152#[derive(Clone)]
a6e00ea6 153struct InstallerState {
ed191e60 154 options: InstallerOptions,
4296d200 155 setup_info: SetupInfo,
2473d5dc 156 runtime_info: RuntimeInfo,
4296d200 157 locales: LocaleInfo,
6ab6af4c 158 steps: HashMap<InstallerStep, ScreenId>,
cae15523 159 in_test_mode: bool,
ed191e60
CH
160}
161
183e2a76
CH
162fn main() {
163 let mut siv = cursive::termion();
164
cae15523
CH
165 let in_test_mode = match env::args().nth(1).as_deref() {
166 Some("-t") => true,
167
168 // Always force the test directory in debug builds
c0706374 169 _ => cfg!(debug_assertions),
cae15523
CH
170 };
171
2f0b4f7c 172 let (setup_info, locales, runtime_info) = match installer_setup(in_test_mode) {
4296d200
CH
173 Ok(result) => result,
174 Err(err) => initial_setup_error(&mut siv, &err),
175 };
130fb96a 176
ccf3b075
CH
177 siv.clear_global_callbacks(Event::CtrlChar('c'));
178 siv.set_on_pre_event(Event::CtrlChar('c'), trigger_abort_install_dialog);
179
a6e00ea6 180 siv.set_user_data(InstallerState {
ed191e60 181 options: InstallerOptions {
5dd6b6ed 182 bootdisk: BootdiskOptions::defaults_from(&runtime_info.disks[0]),
0e42d278 183 timezone: TimezoneOptions::defaults_from(&runtime_info, &locales),
d09fe568 184 password: Default::default(),
2f0b4f7c 185 network: NetworkOptions::defaults_from(&setup_info, &runtime_info.network),
f81ba3ed 186 autoreboot: false,
e9557e62 187 },
2f0b4f7c 188 setup_info,
2473d5dc 189 runtime_info,
4296d200 190 locales,
6ab6af4c 191 steps: HashMap::new(),
cae15523 192 in_test_mode,
e9557e62
CH
193 });
194
6ab6af4c 195 switch_to_next_screen(&mut siv, InstallerStep::Licence, &license_dialog);
183e2a76
CH
196 siv.run();
197}
198
79ae84de
CH
199/// Anything that can be done late in the setup and will not result in fatal errors.
200fn installer_setup_late(siv: &mut Cursive) {
636da45e 201 let state = siv.user_data::<InstallerState>().cloned().unwrap();
79ae84de
CH
202
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) {
082f3a70 207 display_setup_warning(siv, &format!("Failed to apply keyboard layout: {err}"));
79ae84de
CH
208 }
209 }
210 }
082f3a70 211
f9960aa7
NU
212 if state.runtime_info.total_memory < 1024 {
213 display_setup_warning(
214 siv,
215 concat!(
216 "Less than 1 GiB of usable memory detected, installation will probably fail.\n\n",
217 "See 'System Requirements' in the documentation."
218 ),
219 );
220 }
221
636da45e
CH
222 if state.setup_info.config.product == ProxmoxProduct::PVE && !state.runtime_info.hvm_supported {
223 display_setup_warning(
224 siv,
225 concat!(
226 "No support for hardware-accelerated KVM virtualization detected.\n\n",
227 "Check BIOS settings for Intel VT / AMD-V / SVM."
228 ),
229 );
082f3a70 230 }
79ae84de
CH
231}
232
4296d200
CH
233fn initial_setup_error(siv: &mut CursiveRunnable, message: &str) -> ! {
234 siv.add_layer(
235 Dialog::around(TextView::new(message))
236 .title("Installer setup error")
237 .button("Ok", Cursive::quit),
238 );
239 siv.run();
240
241 std::process::exit(1);
242}
243
082f3a70
CH
244fn display_setup_warning(siv: &mut Cursive, message: &str) {
245 siv.add_layer(Dialog::info(message).title("Warning"));
246}
247
6ab6af4c
CH
248fn switch_to_next_screen(
249 siv: &mut Cursive,
250 step: InstallerStep,
251 constructor: &dyn Fn(&mut Cursive) -> InstallerView,
252) {
08ad8ed6 253 let state = siv.user_data::<InstallerState>().cloned().unwrap();
79ae84de 254 let is_first_screen = state.steps.is_empty();
08ad8ed6 255
6ab6af4c 256 // Check if the screen already exists; if yes, then simply switch to it.
08ad8ed6
CH
257 if let Some(screen_id) = state.steps.get(&step) {
258 siv.set_screen(*screen_id);
93f4fdfa
CH
259
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);
268 }
269
08ad8ed6 270 return;
6ab6af4c
CH
271 }
272
5e73fcfe 273 let v = constructor(siv);
6ab6af4c
CH
274 let screen = siv.add_active_screen();
275 siv.with_user_data(|state: &mut InstallerState| state.steps.insert(step, screen));
08ad8ed6 276
1936156e
CH
277 siv.screen_mut().add_transparent_layer_at(
278 XY {
279 x: Offset::Parent(0),
280 y: Offset::Parent(0),
281 },
282 InstallerBackgroundView::new(),
283 );
08ad8ed6 284
5e73fcfe 285 siv.screen_mut().add_layer(v);
79ae84de
CH
286
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.
290 if is_first_screen {
291 installer_setup_late(siv);
292 }
183e2a76
CH
293}
294
64220ff1
CH
295fn switch_to_prev_screen(siv: &mut Cursive) {
296 let id = siv.active_screen().saturating_sub(1);
297 siv.set_screen(id);
298}
299
183e2a76
CH
300fn yes_no_dialog(
301 siv: &mut Cursive,
302 title: &str,
303 text: &str,
deebe07f
CH
304 callback_yes: Box<dyn Fn(&mut Cursive)>,
305 callback_no: Box<dyn Fn(&mut Cursive)>,
183e2a76
CH
306) {
307 siv.add_layer(
308 Dialog::around(TextView::new(text))
309 .title(title)
deebe07f
CH
310 .button("No", move |siv| {
311 siv.pop_layer();
312 callback_no(siv);
313 })
314 .button("Yes", move |siv| {
315 siv.pop_layer();
316 callback_yes(siv);
317 }),
183e2a76
CH
318 )
319}
320
ccf3b075 321fn trigger_abort_install_dialog(siv: &mut Cursive) {
58869243
CH
322 #[cfg(debug_assertions)]
323 siv.quit();
324
325 #[cfg(not(debug_assertions))]
ccf3b075
CH
326 yes_no_dialog(
327 siv,
328 "Abort installation?",
329 "Are you sure you want to abort the installation?",
deebe07f
CH
330 Box::new(Cursive::quit),
331 Box::new(|_| {}),
ccf3b075
CH
332 )
333}
334
183e2a76 335fn abort_install_button() -> Button {
ccf3b075 336 Button::new("Abort", trigger_abort_install_dialog)
183e2a76
CH
337}
338
8c9af1e7
CH
339fn get_eula(setup: &SetupInfo) -> String {
340 let mut path = setup.locations.iso.clone();
341 path.push("EULA");
342
343 std::fs::read_to_string(path)
8f5fdd21 344 .unwrap_or_else(|_| "< Debug build - ignoring non-existing EULA >".to_owned())
183e2a76
CH
345}
346
90c0ea37
CH
347fn license_dialog(siv: &mut Cursive) -> InstallerView {
348 let state = siv.user_data::<InstallerState>().unwrap();
349
1d8fcd72
DC
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)
355 }));
356 let _ = bbar.set_focus_index(2); // ignore errors
357
358 let mut inner = LinearLayout::vertical()
183e2a76
CH
359 .child(PaddedView::lrtb(
360 0,
361 0,
362 1,
363 0,
364 TextView::new("END USER LICENSE AGREEMENT (EULA)").center(),
365 ))
8b43c2d3 366 .child(Panel::new(ScrollView::new(
8c9af1e7 367 TextView::new(get_eula(&state.setup_info)).center(),
183e2a76 368 )))
1d8fcd72
DC
369 .child(PaddedView::lrtb(1, 1, 1, 0, bbar));
370
371 let _ = inner.set_focus_index(2); // ignore errors
183e2a76 372
90c0ea37 373 InstallerView::with_raw(state, inner)
183e2a76
CH
374}
375
e70f1b2f 376fn bootdisk_dialog(siv: &mut Cursive) -> InstallerView {
a6e00ea6 377 let state = siv.user_data::<InstallerState>().cloned().unwrap();
64220ff1 378
f522afb1 379 InstallerView::new(
90c0ea37 380 &state,
521dfff5 381 BootdiskOptionsView::new(siv, &state.runtime_info, &state.options.bootdisk)
ed191e60 382 .with_name("bootdisk-options"),
f522afb1 383 Box::new(|siv| {
994c4ff0
CH
384 let options = siv.call_on_name("bootdisk-options", BootdiskOptionsView::get_values);
385
386 match options {
387 Some(Ok(options)) => {
388 siv.with_user_data(|state: &mut InstallerState| {
389 state.options.bootdisk = options;
390 });
391
392 switch_to_next_screen(siv, InstallerStep::Timezone, &timezone_dialog);
393 }
394
395 Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
396 _ => siv.add_layer(Dialog::info("Invalid values")),
93ebe7bd 397 }
f522afb1 398 }),
1d8fcd72 399 true,
f522afb1 400 )
64220ff1
CH
401}
402
a142f457 403fn timezone_dialog(siv: &mut Cursive) -> InstallerView {
90c0ea37
CH
404 let state = siv.user_data::<InstallerState>().unwrap();
405 let options = &state.options.timezone;
a142f457 406
a142f457 407 InstallerView::new(
90c0ea37 408 state,
e1a11f79 409 TimezoneOptionsView::new(&state.locales, options).with_name("timezone-options"),
a142f457 410 Box::new(|siv| {
e1a11f79 411 let options = siv.call_on_name("timezone-options", TimezoneOptionsView::get_values);
93ebe7bd 412
767843f9
CH
413 match options {
414 Some(Ok(options)) => {
a6e00ea6
CH
415 siv.with_user_data(|state: &mut InstallerState| {
416 state.options.timezone = options;
767843f9
CH
417 });
418
6ab6af4c 419 switch_to_next_screen(siv, InstallerStep::Password, &password_dialog);
767843f9
CH
420 }
421 Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
422 _ => siv.add_layer(Dialog::info("Invalid values")),
93ebe7bd 423 }
a142f457 424 }),
1d8fcd72 425 true,
a142f457 426 )
183e2a76 427}
c2eee468
CH
428
429fn password_dialog(siv: &mut Cursive) -> InstallerView {
90c0ea37
CH
430 let state = siv.user_data::<InstallerState>().unwrap();
431 let options = &state.options.password;
c2eee468 432
15832d18
CH
433 let inner = FormView::new()
434 .child("Root password", EditView::new().secret())
435 .child("Confirm root password", EditView::new().secret())
90c0ea37 436 .child(
0f39ba93 437 "Administrator email",
90c0ea37
CH
438 EditView::new().content(&options.email),
439 )
ef4957e8 440 .with_name("password-options");
c2eee468 441
ccd34284 442 InstallerView::new(
90c0ea37 443 state,
ccd34284
CH
444 inner,
445 Box::new(|siv| {
15832d18
CH
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")?;
450
451 let confirm_password = view
452 .get_value::<EditView, _>(1)
453 .ok_or("failed to retrieve password confirmation")?;
ef4957e8 454
15832d18
CH
455 let email = view
456 .get_value::<EditView, _>(2)
457 .ok_or("failed to retrieve email")?;
ef4957e8 458
298a4de0
DC
459 let email_regex =
460 Regex::new(r"^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$")
461 .unwrap();
462
b82fff5d 463 if root_password.len() < 5 {
fe64752d 464 Err("password too short, must be at least 5 characters long")
b82fff5d 465 } else if root_password != confirm_password {
ef4957e8 466 Err("passwords do not match")
03888174 467 } else if email == "mail@example.invalid" {
b82fff5d 468 Err("invalid email address")
298a4de0
DC
469 } else if !email_regex.is_match(&email) {
470 Err("Email does not look like a valid address (user@domain.tld)")
ef4957e8
CH
471 } else {
472 Ok(PasswordOptions {
473 root_password,
15832d18 474 email,
ef4957e8
CH
475 })
476 }
477 });
478
479 match options {
480 Some(Ok(options)) => {
a6e00ea6
CH
481 siv.with_user_data(|state: &mut InstallerState| {
482 state.options.password = options;
ef4957e8
CH
483 });
484
6ab6af4c 485 switch_to_next_screen(siv, InstallerStep::Network, &network_dialog);
ef4957e8
CH
486 }
487 Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
488 _ => siv.add_layer(Dialog::info("Invalid values")),
489 }
ccd34284 490 }),
1d8fcd72 491 false,
ccd34284
CH
492 )
493}
494
495fn network_dialog(siv: &mut Cursive) -> InstallerView {
90c0ea37
CH
496 let state = siv.user_data::<InstallerState>().unwrap();
497 let options = &state.options.network;
55e5c9cd
TL
498 let ifaces = state.runtime_info.network.interfaces.values();
499 let ifnames = ifaces
500 .clone()
501 .map(|iface| (iface.render(), iface.name.clone()));
89dfa5c9
TL
502 let mut ifaces_selection = SelectView::new().popup().with_all(ifnames.clone());
503
504 ifaces_selection.sort();
505 ifaces_selection.set_selection(
506 ifnames
507 .clone()
57161139 508 .position(|iface| iface.1 == options.ifname)
89dfa5c9
TL
509 .unwrap_or(ifaces.len() - 1),
510 );
ccd34284 511
617f2534 512 let inner = FormView::new()
89dfa5c9 513 .child("Management interface", ifaces_selection)
95c49008
CH
514 .child(
515 "Hostname (FQDN)",
516 EditView::new().content(options.fqdn.to_string()),
517 )
617f2534 518 .child(
ccd34284 519 "IP address (CIDR)",
90c0ea37 520 CidrAddressEditView::new().content(options.address.clone()),
617f2534
CH
521 )
522 .child(
ccd34284
CH
523 "Gateway address",
524 EditView::new().content(options.gateway.to_string()),
617f2534
CH
525 )
526 .child(
1397feec 527 "DNS server address",
ccd34284 528 EditView::new().content(options.dns_server.to_string()),
617f2534 529 )
cd383717 530 .with_name("network-options");
ccd34284 531
947fe360 532 InstallerView::new(
90c0ea37 533 state,
947fe360
CH
534 inner,
535 Box::new(|siv| {
617f2534
CH
536 let options = siv.call_on_name("network-options", |view: &mut FormView| {
537 let ifname = view
538 .get_value::<SelectView, _>(0)
539 .ok_or("failed to retrieve management interface name")?;
cd383717 540
617f2534
CH
541 let fqdn = view
542 .get_value::<EditView, _>(1)
95c49008
CH
543 .ok_or("failed to retrieve host FQDN")?
544 .parse::<Fqdn>()
55dc67ca 545 .map_err(|err| format!("hostname does not look valid:\n\n{err}"))?;
617f2534
CH
546
547 let address = view
548 .get_value::<CidrAddressEditView, _>(2)
549 .ok_or("failed to retrieve host address")?;
550
551 let gateway = view
552 .get_value::<EditView, _>(3)
553 .ok_or("failed to retrieve gateway address")?
554 .parse::<IpAddr>()
555 .map_err(|err| err.to_string())?;
556
557 let dns_server = view
d2b13ba7 558 .get_value::<EditView, _>(4)
617f2534
CH
559 .ok_or("failed to retrieve DNS server address")?
560 .parse::<IpAddr>()
561 .map_err(|err| err.to_string())?;
562
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())
95c49008
CH
567 } else if fqdn.to_string().ends_with(".invalid") {
568 Err("hostname does not look valid".to_owned())
617f2534
CH
569 } else {
570 Ok(NetworkOptions {
571 ifname,
572 fqdn,
573 address,
574 gateway,
575 dns_server,
576 })
577 }
cd383717
CH
578 });
579
617f2534
CH
580 match options {
581 Some(Ok(options)) => {
a6e00ea6
CH
582 siv.with_user_data(|state: &mut InstallerState| {
583 state.options.network = options;
617f2534 584 });
cd383717 585
6ab6af4c 586 switch_to_next_screen(siv, InstallerStep::Summary, &summary_dialog);
617f2534
CH
587 }
588 Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
589 _ => siv.add_layer(Dialog::info("Invalid values")),
cd383717 590 }
947fe360 591 }),
1d8fcd72 592 true,
947fe360
CH
593 )
594}
595
b7828c87 596pub struct SummaryOption {
947fe360
CH
597 name: &'static str,
598 value: String,
599}
600
601impl SummaryOption {
602 pub fn new<S: Into<String>>(name: &'static str, value: S) -> Self {
603 Self {
604 name,
605 value: value.into(),
606 }
607 }
608}
609
610impl TableViewItem for SummaryOption {
611 fn get_column(&self, name: &str) -> String {
612 match name {
613 "name" => self.name.to_owned(),
614 "value" => self.value.clone(),
615 _ => unreachable!(),
616 }
617 }
618}
619
620fn summary_dialog(siv: &mut Cursive) -> InstallerView {
90c0ea37 621 let state = siv.user_data::<InstallerState>().unwrap();
947fe360 622
1d8fcd72
DC
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))
627 .child(DummyView)
628 .child(Button::new("Install", |siv| {
629 let autoreboot = siv
630 .find_name("reboot-after-install")
631 .map(|v: ViewRef<Checkbox>| v.is_checked())
632 .unwrap_or_default();
633
634 siv.with_user_data(|state: &mut InstallerState| {
635 state.options.autoreboot = autoreboot;
636 });
637
638 switch_to_next_screen(siv, InstallerStep::Install, &install_progress_dialog);
639 }));
640
fcb51d73 641 let _ = bbar.set_focus_index(2); // ignore errors
1d8fcd72
DC
642
643 let mut inner = LinearLayout::vertical()
947fe360
CH
644 .child(PaddedView::lrtb(
645 0,
646 0,
647 1,
648 2,
649 TableView::new()
650 .columns(&[
651 ("name".to_owned(), "Option".to_owned()),
652 ("value".to_owned(), "Selected value".to_owned()),
653 ])
b107a892 654 .items(state.options.to_summary(&state.locales)),
947fe360
CH
655 ))
656 .child(
657 LinearLayout::horizontal()
658 .child(DummyView.full_width())
a84f277b 659 .child(Checkbox::new().checked().with_name("reboot-after-install"))
947fe360
CH
660 .child(
661 TextView::new(" Automatically reboot after successful installation").no_wrap(),
662 )
663 .child(DummyView.full_width()),
664 )
1d8fcd72 665 .child(PaddedView::lrtb(1, 1, 1, 0, bbar));
ea15ca43 666
1d8fcd72 667 let _ = inner.set_focus_index(2); // ignore errors
947fe360 668
90c0ea37 669 InstallerView::with_raw(state, inner)
c2eee468 670}
81f9348c
CH
671
672fn install_progress_dialog(siv: &mut Cursive) -> InstallerView {
673 // Ensure the screen is updated independently of keyboard events and such
674 siv.set_autorefresh(true);
675
3285f0ad 676 let cb_sink = siv.cb_sink().clone();
81f9348c 677 let state = siv.user_data::<InstallerState>().unwrap();
a56e078a 678 let in_test_mode = state.in_test_mode;
3285f0ad
CH
679 let progress_text = TextContent::new("starting the installation ..");
680
681 let progress_task = {
682 let progress_text = progress_text.clone();
683 let options = state.options.clone();
684 move |counter: Counter| {
685 let child = {
686 use std::process::{Command, Stdio};
687
a56e078a
CH
688 let (path, args, envs): (&str, &[&str], Vec<(&str, &str)>) = if in_test_mode {
689 (
690 "./proxmox-low-level-installer",
691 &["-t", "start-session-test"],
692 vec![("PERL5LIB", ".")],
693 )
694 } else {
695 ("proxmox-low-level-installer", &["start-session"], vec![])
696 };
3285f0ad
CH
697
698 Command::new(path)
699 .args(args)
700 .envs(envs)
701 .stdin(Stdio::piped())
702 .stdout(Stdio::piped())
703 .spawn()
704 };
705
706 let mut child = match child {
707 Ok(child) => child,
708 Err(err) => {
709 let _ = cb_sink.send(Box::new(move |siv| {
710 siv.add_layer(
711 Dialog::text(err.to_string())
712 .title("Error")
713 .button("Ok", Cursive::quit),
714 );
715 }));
716 return;
717 }
718 };
719
720 let inner = || {
721 let reader = child.stdout.take().map(BufReader::new)?;
722 let mut writer = child.stdin.take()?;
723
724 serde_json::to_writer(&mut writer, &InstallConfig::from(options)).unwrap();
725 writeln!(writer).unwrap();
726
727 let writer = Arc::new(Mutex::new(writer));
728
729 for line in reader.lines() {
730 let line = match line {
731 Ok(line) => line,
732 Err(_) => break,
733 };
734
735 let msg = match line.parse::<UiMessage>() {
736 Ok(msg) => msg,
0fd882c3
CH
737 Err(stray) => {
738 eprintln!("low-level installer: {stray}");
3285f0ad
CH
739 continue;
740 }
741 };
742
743 match msg {
744 UiMessage::Info(s) => cb_sink.send(Box::new(|siv| {
745 siv.add_layer(Dialog::info(s).title("Information"));
746 })),
747 UiMessage::Error(s) => cb_sink.send(Box::new(|siv| {
748 siv.add_layer(Dialog::info(s).title("Error"));
749 })),
750 UiMessage::Prompt(s) => cb_sink.send({
751 let writer = writer.clone();
752 Box::new(move |siv| {
753 yes_no_dialog(
754 siv,
755 "Prompt",
756 &s,
757 Box::new({
758 let writer = writer.clone();
759 move |_| {
760 if let Ok(mut writer) = writer.lock() {
761 let _ = writeln!(writer, "ok");
762 }
763 }
764 }),
765 Box::new(move |_| {
766 if let Ok(mut writer) = writer.lock() {
767 let _ = writeln!(writer);
768 }
769 }),
770 );
771 })
772 }),
773 UiMessage::Progress(ratio, s) => {
774 counter.set(ratio);
775 progress_text.set_content(s);
776 Ok(())
777 }
a8fbe0ff
TL
778 UiMessage::Finished(success, msg) => {
779 counter.set(100);
780 progress_text.set_content(msg.to_owned());
781 cb_sink.send(Box::new(move |siv| {
782 let title = if success { "Success" } else { "Failure" };
7111932d
CH
783
784 // For rebooting, we just need to quit the installer,
785 // our caller does the actual reboot.
a8fbe0ff 786 siv.add_layer(
4a8f0a1c
CH
787 Dialog::text(msg)
788 .title(title)
638479b9 789 .button("Reboot now", Cursive::quit),
a8fbe0ff 790 );
7111932d
CH
791
792 let autoreboot = siv
793 .user_data::<InstallerState>()
794 .map(|state| state.options.autoreboot)
795 .unwrap_or_default();
796
e1eb2ac9 797 if autoreboot && success {
7111932d
CH
798 let cb_sink = siv.cb_sink();
799 thread::spawn({
800 let cb_sink = cb_sink.clone();
801 move || {
802 thread::sleep(Duration::from_secs(5));
803 let _ = cb_sink.send(Box::new(Cursive::quit));
804 }
805 });
806 }
a8fbe0ff
TL
807 }))
808 }
3285f0ad
CH
809 }
810 .unwrap();
81f9348c 811 }
3285f0ad 812
3285f0ad
CH
813 Some(())
814 };
815
816 if inner().is_none() {
817 cb_sink
818 .send(Box::new(|siv| {
819 siv.add_layer(
820 Dialog::text("low-level installer exited early")
821 .title("Error")
822 .button("Exit", Cursive::quit),
823 );
824 }))
825 .unwrap();
81f9348c 826 }
3285f0ad
CH
827 }
828 };
81f9348c 829
3285f0ad 830 let progress_bar = ProgressBar::new().with_task(progress_task).full_width();
81f9348c
CH
831 let inner = PaddedView::lrtb(
832 1,
833 1,
834 1,
835 1,
836 LinearLayout::vertical()
837 .child(PaddedView::lrtb(1, 1, 0, 0, progress_bar))
838 .child(DummyView)
e2a1e0c2
CH
839 .child(TextView::new_with_content(progress_text).center())
840 .child(PaddedView::lrtb(
841 1,
842 1,
843 1,
844 0,
845 LinearLayout::horizontal().child(abort_install_button()),
846 )),
81f9348c
CH
847 );
848
849 InstallerView::with_raw(state, inner)
850}
3285f0ad
CH
851
852enum UiMessage {
853 Info(String),
854 Error(String),
855 Prompt(String),
a8fbe0ff 856 Finished(bool, String),
3285f0ad
CH
857 Progress(usize, String),
858}
859
860impl FromStr for UiMessage {
861 type Err = String;
862
863 fn from_str(s: &str) -> Result<Self, Self::Err> {
864 let (ty, rest) = s.split_once(": ").ok_or("invalid message: no type")?;
865
866 match ty {
867 "message" => Ok(UiMessage::Info(rest.to_owned())),
868 "error" => Ok(UiMessage::Error(rest.to_owned())),
869 "prompt" => Ok(UiMessage::Prompt(rest.to_owned())),
a8fbe0ff
TL
870 "finished" => {
871 let (state, rest) = rest.split_once(", ").ok_or("invalid message: no state")?;
872 Ok(UiMessage::Finished(state == "ok", rest.to_owned()))
873 }
3285f0ad
CH
874 "progress" => {
875 let (percent, rest) = rest.split_once(' ').ok_or("invalid progress message")?;
876 Ok(UiMessage::Progress(
877 percent
878 .parse::<f64>()
a8fbe0ff 879 .map(|v| (v * 100.).floor() as usize)
3285f0ad
CH
880 .map_err(|err| err.to_string())?,
881 rest.to_owned(),
882 ))
883 }
41f03d90 884 unknown => Err(format!("invalid message type {unknown}, rest: {rest}")),
3285f0ad
CH
885 }
886 }
887}